diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 748bec74a..0f8a0659b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,7 @@ cache: stages: - build - test + - benchmark - deploy - release @@ -28,6 +29,20 @@ build: - mix deps.get - mix compile --force +benchmark: + stage: benchmark + variables: + MIX_ENV: benchmark + services: + - name: lainsoykaf/postgres-with-rum + 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 + - mix pleroma.load_testing + unit-testing: stage: test services: @@ -70,7 +85,7 @@ docs-deploy: stage: deploy image: alpine:latest only: - - master@pleroma/pleroma + - stable@pleroma/pleroma - develop@pleroma/pleroma before_script: - apk add curl @@ -127,9 +142,10 @@ amd64: # TODO: Replace with upstream image when 1.9.0 comes out image: rinpatch/elixir:1.9.0-rc.0 only: &release-only - - master@pleroma/pleroma + - stable@pleroma/pleroma - develop@pleroma/pleroma - /^maint/.*$/@pleroma/pleroma + - /^release/.*$/@pleroma/pleroma artifacts: &release-artifacts name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME" paths: diff --git a/CHANGELOG.md b/CHANGELOG.md index 31aa567b6..4f47f88de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,44 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Removed +- **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media` +- **Breaking**: OStatus protocol support + +### Changed +- **Breaking:** Elixir >=1.8 is now required (was >= 1.7) +- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings) +- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler +- Enabled `:instance, extended_nickname_format` in the default config +- Add `rel="ugc"` to all links in statuses, to prevent SEO spam +- Extract RSS functionality from OStatus +- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities +- OStatus: Extract RSS functionality +- Deprecated `User.Info` embedded schema (fields moved to `User`) +- Store status data inside Flag activity +
+ API Changes + +- **Breaking** Admin API: `PATCH /api/pleroma/admin/users/:nickname/force_password_reset` is now `PATCH /api/pleroma/admin/users/force_password_reset` (accepts `nicknames` array in the request body) +- **Breaking:** Admin API: Return link alongside with token on password reset +- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string. +- Admin API: Return `total` when querying for reports +- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) +- Admin API: Return link alongside with token on password reset +- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`) +- Mastodon API: `pleroma.thread_muted` to the Status entity +- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message +- Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload. +
+ ### Added - Refreshing poll results for remote polls +- Authentication: Added rate limit for password-authorized actions / login existence checks +- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`) +- 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). +
+ API Changes + - Job queue stats to the healthcheck page - Admin API: Add ability to require password reset - Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition) @@ -14,30 +50,46 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Add `upload_limit`, `avatar_upload_limit`, `background_upload_limit`, and `banner_upload_limit` to `/api/v1/instance` - Mastodon API: Add `pleroma.unread_conversation_count` to the Account entity - OAuth: support for hierarchical permissions / [Mastodon 2.4.3 OAuth permissions](https://docs.joinmastodon.org/api/permissions/) -- Authentication: Added rate limit for password-authorized actions / login existence checks - Metadata Link: Atom syndication Feed - Mix task to re-count statuses for all users (`mix pleroma.count_statuses`) +- Mastodon API: Add `exclude_visibilities` parameter to the timeline and notification endpoints +- Admin API: `/users/:nickname/toggle_activation` endpoint is now deprecated in favor of: `/users/activate`, `/users/deactivate`, both accept `nicknames` array +- Admin API: `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group` (both accept `nicknames` array), `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 +- Mastodon API: Add `/api/v1/markers` for managing timeline read markers +- Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations` - Pleroma API: Add Emoji reactions - -### Changed -- **Breaking:** Elixir >=1.8 is now required (was >= 1.7) -- **Breaking:** Admin API: Return link alongside with token on password reset -- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings) -- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler -- Admin API: Return `total` when querying for reports -- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) -- Admin API: Return link alongside with token on password reset -- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities -- OStatus: Extract RSS functionality -- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`) +
### Fixed +- Report emails now include functional links to profiles of remote user accounts +
+ 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` -- Added `:instance, extended_nickname_format` setting to the default config -- Report emails now include functional links to profiles of remote user accounts +
-## [1.1.0] - 2019-??-?? +## [1.1.2] - 2019-10-18 +### Fixed +- `pleroma_ctl` trying to connect to a running instance when generating the config, which of course doesn't exist. + +## [1.1.1] - 2019-10-18 +### Fixed +- One of the migrations between 1.0.0 and 1.1.0 wiping user info of the relay user because of unexpected behavior of postgresql's `jsonb_set`, resulting in inability to post in the default configuration. If you were affected, please run the following query in postgres console, the relay user will be recreated automatically: +``` +delete from users where ap_id = 'https://your.instance.hostname/relay'; +``` +- Bad user search matches + +## [1.1.0] - 2019-10-14 +**Breaking:** The stable branch has been changed from `master` to `stable`. If you want to keep using 1.0, the `release/1.0` branch will receive security updates for 6 months after 1.1 release. + +**OTP Note:** `pleroma_ctl` in 1.0 defaults to `master` and doesn't support specifying arbitrary branches, making `./pleroma_ctl update` fail. To fix this, fetch a version of `pleroma_ctl` from 1.1 using the command below and proceed with the update normally: +``` +curl -Lo ./bin/pleroma_ctl 'https://git.pleroma.social/pleroma/pleroma/raw/develop/rel/files/bin/pleroma_ctl' +``` ### Security - Mastodon API: respect post privacy in `/api/v1/statuses/:id/{favourited,reblogged}_by` @@ -45,16 +97,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** GNU Social API with Qvitter extensions support - Emoji: Remove longfox emojis. - Remove `Reply-To` header from report emails for admins. +- ActivityPub: The `/objects/:uuid/likes` endpoint. ### Changed - **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config - **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired - **Breaking:** `/api/pleroma/notifications/read` is moved to `/api/v1/pleroma/notifications/read` and now supports `max_id` and responds with Mastodon API entities. -- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string. - Configuration: added `config/description.exs`, from which `docs/config.md` is generated - Configuration: OpenGraph and TwitterCard providers enabled by default - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text -- Mastodon API: `pleroma.thread_muted` key in the Status entity - Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set - NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option - NodeInfo: Return `mailerEnabled` in `metadata` @@ -63,7 +114,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses) - Improve digest email template – Pagination: (optional) return `total` alongside with `items` when paginating -- Add `rel="ugc"` to all links in statuses, to prevent SEO spam +- The `Pleroma.FlakeId` module has been replaced with the `flake_id` library. ### Fixed - Following from Osada @@ -74,21 +125,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Misskey's endless polls being unable to render - Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity - Mastodon API: Notifications endpoint crashing if one notification failed to render +- Mastodon API: `exclude_replies` is correctly handled again. - Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`) - Mastodon API, streaming: Fix filtering of notifications based on blocks/mutes/thread mutes -- ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set -- Existing user id not being preserved on insert conflict +- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`) +- Mastodon API: Ensure the `account` field is not empty when rendering Notification entities. +- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname` +- Mastodon API: Blocks are now treated consistently between the Streaming API and the Timeline APIs - Rich Media: Parser failing when no TTL can be found by image TTL setters - Rich Media: The crawled URL is now spliced into the rich media data. - ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification. -- Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected. -- Report email not being sent to admins when the reporter is a remote user -- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances +- ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set - ActivityPub: Deactivated user deletion - ActivityPub: Fix `/users/:nickname/inbox` crashing without an authenticated user - MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled -- Mastodon API: Blocks are now treated consistently between the Streaming API and the Timeline APIs -- Mastodon API: `exclude_replies` is correctly handled again. +- ActivityPub: Correct addressing of Undo. +- ActivityPub: Correct addressing of profile update activities. +- ActivityPub: Polls are now refreshed when necessary. +- Report emails now include functional links to profiles of remote user accounts +- Existing user id not being preserved on insert conflict +- Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected. +- Report email not being sent to admins when the reporter is a remote user +- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances ### Added - Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically. @@ -102,6 +160,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Support for the [`tagged` filter](https://github.com/tootsuite/mastodon/pull/9755) in [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/api/rest/accounts/#get-api-v1-accounts-id-statuses) - Mastodon API, streaming: Add support for passing the token in the `Sec-WebSocket-Protocol` header - Mastodon API, extension: Ability to reset avatar, profile banner, and background +- Mastodon API: Add support for `fields_attributes` API parameter (setting custom fields) - Mastodon API: Add support for categories for custom emojis by reusing the group feature. - Mastodon API: Add support for muting/unmuting notifications - Mastodon API: Add support for the `blocked_by` attribute in the relationship API (`GET /api/v1/accounts/relationships`). @@ -110,7 +169,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: added `/auth/password` endpoint for password reset with rate limit. - Mastodon API: /api/v1/accounts/:id/statuses now supports nicknames or user id - Mastodon API: Improve support for the user profile custom fields -- Mastodon API: follower/following counters are nullified when `hide_follows`/`hide_followers` and `hide_follows_count`/`hide_followers_count` are set +- Mastodon API: Add support for `fields_attributes` API parameter (setting custom fields) +- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`) - Admin API: Return users' tags when querying reports - Admin API: Return avatar and display name when querying users - Admin API: Allow querying user by ID @@ -128,11 +188,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=` for resending account confirmation. - Pleroma API: Email change endpoint. - Admin API: Added moderation log -- 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). - Web response cache (currently, enabled for ActivityPub) -- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`) -- ActivityPub: Add ActivityPub actor's `discoverable` parameter. -- Admin API: Added moderation log filters (user/start date/end date/search/pagination) - Reverse Proxy: Do not retry failed requests to limit pressure on the peer ### Changed diff --git a/README.md b/README.md index 846442346..dd49822e9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ While we don’t provide docker files, other people have written very good ones. ### Dependencies -* Postgresql version 9.6 or newer +* Postgresql version 9.6 or newer, including the contrib modules * Elixir version 1.7 or newer. If your distribution only has an old version available, check [Elixir’s install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf). * Build-essential tools @@ -71,7 +71,7 @@ This is useful for running Pleroma inside Tor or I2P. ## Customization and contribution -The [Pleroma Documentation](https://docs-develop.pleroma.social/readme.html) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project. +The [Pleroma Documentation](https://docs-develop.pleroma.social) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project. ## Troubleshooting diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex new file mode 100644 index 000000000..cdc073b2e --- /dev/null +++ b/benchmarks/load_testing/fetcher.ex @@ -0,0 +1,231 @@ +defmodule Pleroma.LoadTesting.Fetcher do + use Pleroma.LoadTesting.Helper + + def fetch_user(user) do + Benchee.run(%{ + "By id" => fn -> Repo.get_by(User, id: user.id) end, + "By ap_id" => fn -> Repo.get_by(User, ap_id: user.ap_id) end, + "By email" => fn -> Repo.get_by(User, email: user.email) end, + "By nickname" => fn -> Repo.get_by(User, nickname: user.nickname) end + }) + end + + def query_timelines(user) do + home_timeline_params = %{ + "count" => 20, + "with_muted" => true, + "type" => ["Create", "Announce"], + "blocking_user" => user, + "muting_user" => user, + "user" => user + } + + mastodon_public_timeline_params = %{ + "count" => 20, + "local_only" => true, + "only_media" => "false", + "type" => ["Create", "Announce"], + "with_muted" => "true", + "blocking_user" => user, + "muting_user" => user + } + + mastodon_federated_timeline_params = %{ + "count" => 20, + "only_media" => "false", + "type" => ["Create", "Announce"], + "with_muted" => "true", + "blocking_user" => user, + "muting_user" => user + } + + following = User.following(user) + + Benchee.run(%{ + "User home timeline" => fn -> + Pleroma.Web.ActivityPub.ActivityPub.fetch_activities( + following, + home_timeline_params + ) + end, + "User mastodon public timeline" => fn -> + Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities( + mastodon_public_timeline_params + ) + end, + "User mastodon federated public timeline" => fn -> + Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities( + mastodon_federated_timeline_params + ) + end + }) + + home_activities = + Pleroma.Web.ActivityPub.ActivityPub.fetch_activities( + following, + home_timeline_params + ) + + public_activities = + Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(mastodon_public_timeline_params) + + public_federated_activities = + Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities( + mastodon_federated_timeline_params + ) + + Benchee.run(%{ + "Rendering home timeline" => fn -> + Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ + activities: home_activities, + for: user, + as: :activity + }) + end, + "Rendering public timeline" => fn -> + Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ + activities: public_activities, + for: user, + as: :activity + }) + end, + "Rendering public federated timeline" => fn -> + Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ + activities: public_federated_activities, + for: user, + as: :activity + }) + end + }) + end + + def query_notifications(user) do + without_muted_params = %{"count" => "20", "with_muted" => "false"} + with_muted_params = %{"count" => "20", "with_muted" => "true"} + + Benchee.run(%{ + "Notifications without muted" => fn -> + Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params) + end, + "Notifications with muted" => fn -> + Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params) + end + }) + + without_muted_notifications = + Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params) + + with_muted_notifications = + Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params) + + Benchee.run(%{ + "Render notifications without muted" => fn -> + Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{ + notifications: without_muted_notifications, + for: user + }) + end, + "Render notifications with muted" => fn -> + Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{ + notifications: with_muted_notifications, + for: user + }) + end + }) + end + + def query_dms(user) do + params = %{ + "count" => "20", + "with_muted" => "true", + "type" => "Create", + "blocking_user" => user, + "user" => user, + visibility: "direct" + } + + Benchee.run(%{ + "Direct messages with muted" => fn -> + Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params) + |> Pleroma.Pagination.fetch_paginated(params) + end, + "Direct messages without muted" => fn -> + Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params) + |> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false)) + end + }) + + dms_with_muted = + Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params) + |> Pleroma.Pagination.fetch_paginated(params) + + dms_without_muted = + Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params) + |> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false)) + + Benchee.run(%{ + "Rendering dms with muted" => fn -> + Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ + activities: dms_with_muted, + for: user, + as: :activity + }) + end, + "Rendering dms without muted" => fn -> + Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ + activities: dms_without_muted, + for: user, + as: :activity + }) + end + }) + end + + def query_long_thread(user, activity) do + Benchee.run(%{ + "Fetch main post" => fn -> + Pleroma.Activity.get_by_id_with_object(activity.id) + end, + "Fetch context of main post" => fn -> + Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context( + activity.data["context"], + %{ + "blocking_user" => user, + "user" => user, + "exclude_id" => activity.id + } + ) + end + }) + + activity = Pleroma.Activity.get_by_id_with_object(activity.id) + + context = + Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context( + activity.data["context"], + %{ + "blocking_user" => user, + "user" => user, + "exclude_id" => activity.id + } + ) + + Benchee.run(%{ + "Render status" => fn -> + Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{ + activity: activity, + for: user + }) + end, + "Render context" => fn -> + Pleroma.Web.MastodonAPI.StatusView.render( + "index.json", + for: user, + activities: context, + as: :activity + ) + |> Enum.reverse() + end + }) + end +end diff --git a/benchmarks/load_testing/generator.ex b/benchmarks/load_testing/generator.ex new file mode 100644 index 000000000..b4432bdb7 --- /dev/null +++ b/benchmarks/load_testing/generator.ex @@ -0,0 +1,350 @@ +defmodule Pleroma.LoadTesting.Generator do + use Pleroma.LoadTesting.Helper + alias Pleroma.Web.CommonAPI + + def generate_users(opts) do + IO.puts("Starting generating #{opts[:users_max]} users...") + {time, _} = :timer.tc(fn -> do_generate_users(opts) end) + + IO.puts("Inserting users take #{to_sec(time)} sec.\n") + end + + defp do_generate_users(opts) do + max = Keyword.get(opts, :users_max) + + Task.async_stream( + 1..max, + &generate_user_data(&1), + max_concurrency: 10, + timeout: 30_000 + ) + |> Enum.to_list() + end + + defp generate_user_data(i) do + remote = Enum.random([true, false]) + + user = %User{ + name: "Test テスト User #{i}", + email: "user#{i}@example.com", + nickname: "nick#{i}", + password_hash: + "$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg", + bio: "Tester Number #{i}", + info: %{}, + local: remote + } + + user_urls = + if remote do + base_url = + Enum.random(["https://domain1.com", "https://domain2.com", "https://domain3.com"]) + + ap_id = "#{base_url}/users/#{user.nickname}" + + %{ + ap_id: ap_id, + follower_address: ap_id <> "/followers", + following_address: ap_id <> "/following" + } + else + %{ + ap_id: User.ap_id(user), + follower_address: User.ap_followers(user), + following_address: User.ap_following(user) + } + end + + user = Map.merge(user, user_urls) + + Repo.insert!(user) + end + + def generate_activities(user, users) do + do_generate_activities(user, users) + end + + defp do_generate_activities(user, users) do + IO.puts("Starting generating 20000 common activities...") + + {time, _} = + :timer.tc(fn -> + Task.async_stream( + 1..20_000, + fn _ -> + do_generate_activity([user | users]) + end, + max_concurrency: 10, + timeout: 30_000 + ) + |> Stream.run() + end) + + IO.puts("Inserting common activities take #{to_sec(time)} sec.\n") + + IO.puts("Starting generating 20000 activities with mentions...") + + {time, _} = + :timer.tc(fn -> + Task.async_stream( + 1..20_000, + fn _ -> + do_generate_activity_with_mention(user, users) + end, + max_concurrency: 10, + timeout: 30_000 + ) + |> Stream.run() + end) + + IO.puts("Inserting activities with menthions take #{to_sec(time)} sec.\n") + + IO.puts("Starting generating 10000 activities with threads...") + + {time, _} = + :timer.tc(fn -> + Task.async_stream( + 1..10_000, + fn _ -> + do_generate_threads([user | users]) + end, + max_concurrency: 10, + timeout: 30_000 + ) + |> Stream.run() + end) + + IO.puts("Inserting activities with threads take #{to_sec(time)} sec.\n") + end + + defp do_generate_activity(users) do + post = %{ + "status" => "Some status without mention with random user" + } + + CommonAPI.post(Enum.random(users), post) + end + + defp do_generate_activity_with_mention(user, users) do + mentions_cnt = Enum.random([2, 3, 4, 5]) + with_user = Enum.random([true, false]) + users = Enum.shuffle(users) + mentions_users = Enum.take(users, mentions_cnt) + mentions_users = if with_user, do: [user | mentions_users], else: mentions_users + + mentions_str = + Enum.map(mentions_users, fn user -> "@" <> user.nickname end) |> Enum.join(", ") + + post = %{ + "status" => mentions_str <> "some status with mentions random users" + } + + CommonAPI.post(Enum.random(users), post) + end + + defp do_generate_threads(users) do + thread_length = Enum.random([2, 3, 4, 5]) + actor = Enum.random(users) + + post = %{ + "status" => "Start of the thread" + } + + {:ok, activity} = CommonAPI.post(actor, post) + + Enum.each(1..thread_length, fn _ -> + user = Enum.random(users) + + post = %{ + "status" => "@#{actor.nickname} reply to thread", + "in_reply_to_status_id" => activity.id + } + + CommonAPI.post(user, post) + end) + end + + def generate_remote_activities(user, users) do + do_generate_remote_activities(user, users) + end + + defp do_generate_remote_activities(user, users) do + IO.puts("Starting generating 10000 remote activities...") + + {time, _} = + :timer.tc(fn -> + Task.async_stream( + 1..10_000, + fn i -> + do_generate_remote_activity(i, user, users) + end, + max_concurrency: 10, + timeout: 30_000 + ) + |> Stream.run() + end) + + IO.puts("Inserting remote activities take #{to_sec(time)} sec.\n") + end + + defp do_generate_remote_activity(i, user, users) do + actor = Enum.random(users) + %{host: host} = URI.parse(actor.ap_id) + date = Date.utc_today() + datetime = DateTime.utc_now() + + map = %{ + "actor" => actor.ap_id, + "cc" => [actor.follower_address, user.ap_id], + "context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation", + "id" => actor.ap_id <> "/statuses/#{i}/activity", + "object" => %{ + "actor" => actor.ap_id, + "atomUri" => actor.ap_id <> "/statuses/#{i}", + "attachment" => [], + "attributedTo" => actor.ap_id, + "bcc" => [], + "bto" => [], + "cc" => [actor.follower_address, user.ap_id], + "content" => + "

+ user.ap_id <> + "\" class=\"u-url mention\">@" <> user.nickname <> "

", + "context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation", + "conversation" => + "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation", + "emoji" => %{}, + "id" => actor.ap_id <> "/statuses/#{i}", + "inReplyTo" => nil, + "inReplyToAtomUri" => nil, + "published" => datetime, + "sensitive" => true, + "summary" => "cw", + "tag" => [ + %{ + "href" => user.ap_id, + "name" => "@#{user.nickname}@#{host}", + "type" => "Mention" + } + ], + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Note", + "url" => "http://#{host}/@#{actor.nickname}/#{i}" + }, + "published" => datetime, + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Create" + } + + Pleroma.Web.ActivityPub.ActivityPub.insert(map, false) + end + + def generate_dms(user, users, opts) do + IO.puts("Starting generating #{opts[:dms_max]} DMs") + {time, _} = :timer.tc(fn -> do_generate_dms(user, users, opts) end) + IO.puts("Inserting dms take #{to_sec(time)} sec.\n") + end + + defp do_generate_dms(user, users, opts) do + Task.async_stream( + 1..opts[:dms_max], + fn _ -> + do_generate_dm(user, users) + end, + max_concurrency: 10, + timeout: 30_000 + ) + |> Stream.run() + end + + defp do_generate_dm(user, users) do + post = %{ + "status" => "@#{user.nickname} some direct message", + "visibility" => "direct" + } + + CommonAPI.post(Enum.random(users), post) + end + + def generate_long_thread(user, users, opts) do + IO.puts("Starting generating long thread with #{opts[:thread_length]} replies") + {time, activity} = :timer.tc(fn -> do_generate_long_thread(user, users, opts) end) + IO.puts("Inserting long thread replies take #{to_sec(time)} sec.\n") + {:ok, activity} + end + + defp do_generate_long_thread(user, users, opts) do + {:ok, %{id: id} = activity} = CommonAPI.post(user, %{"status" => "Start of long thread"}) + + Task.async_stream( + 1..opts[:thread_length], + fn _ -> do_generate_thread(users, id) end, + max_concurrency: 10, + timeout: 30_000 + ) + |> Stream.run() + + activity + end + + defp do_generate_thread(users, activity_id) do + CommonAPI.post(Enum.random(users), %{ + "status" => "reply to main post", + "in_reply_to_status_id" => activity_id + }) + end + + def generate_non_visible_message(user, users) do + IO.puts("Starting generating 1000 non visible posts") + + {time, _} = + :timer.tc(fn -> + do_generate_non_visible_posts(user, users) + end) + + IO.puts("Inserting non visible posts take #{to_sec(time)} sec.\n") + end + + defp do_generate_non_visible_posts(user, users) do + [not_friend | users] = users + + make_friends(user, users) + + Task.async_stream(1..1000, fn _ -> do_generate_non_visible_post(not_friend, users) end, + max_concurrency: 10, + timeout: 30_000 + ) + |> Stream.run() + end + + defp make_friends(_user, []), do: nil + + defp make_friends(user, [friend | users]) do + {:ok, _} = User.follow(user, friend) + {:ok, _} = User.follow(friend, user) + make_friends(user, users) + end + + defp do_generate_non_visible_post(not_friend, users) do + post = %{ + "status" => "some non visible post", + "visibility" => "private" + } + + {:ok, activity} = CommonAPI.post(not_friend, post) + + thread_length = Enum.random([2, 3, 4, 5]) + + Enum.each(1..thread_length, fn _ -> + user = Enum.random(users) + + post = %{ + "status" => "@#{not_friend.nickname} reply to non visible post", + "in_reply_to_status_id" => activity.id, + "visibility" => "private" + } + + CommonAPI.post(user, post) + end) + end +end diff --git a/benchmarks/load_testing/helper.ex b/benchmarks/load_testing/helper.ex new file mode 100644 index 000000000..47b25c65f --- /dev/null +++ b/benchmarks/load_testing/helper.ex @@ -0,0 +1,11 @@ +defmodule Pleroma.LoadTesting.Helper do + defmacro __using__(_) do + quote do + import Ecto.Query + alias Pleroma.Repo + alias Pleroma.User + + defp to_sec(microseconds), do: microseconds / 1_000_000 + end + end +end diff --git a/benchmarks/mix/tasks/pleroma/load_testing.ex b/benchmarks/mix/tasks/pleroma/load_testing.ex new file mode 100644 index 000000000..4fa3eec49 --- /dev/null +++ b/benchmarks/mix/tasks/pleroma/load_testing.ex @@ -0,0 +1,134 @@ +defmodule Mix.Tasks.Pleroma.LoadTesting do + use Mix.Task + use Pleroma.LoadTesting.Helper + import Mix.Pleroma + import Pleroma.LoadTesting.Generator + import Pleroma.LoadTesting.Fetcher + + @shortdoc "Factory for generation data" + @moduledoc """ + Generates data like: + - local/remote users + - local/remote activities with notifications + - direct messages + - long thread + - non visible posts + + ## Generate data + MIX_ENV=benchmark mix pleroma.load_testing --users 20000 --dms 20000 --thread_length 2000 + MIX_ENV=benchmark mix pleroma.load_testing -u 20000 -d 20000 -t 2000 + + Options: + - `--users NUMBER` - number of users to generate. Defaults to: 20000. Alias: `-u` + - `--dms NUMBER` - number of direct messages to generate. Defaults to: 20000. Alias `-d` + - `--thread_length` - number of messages in thread. Defaults to: 2000. ALias `-t` + """ + + @aliases [u: :users, d: :dms, t: :thread_length] + @switches [ + users: :integer, + dms: :integer, + thread_length: :integer + ] + @users_default 20_000 + @dms_default 1_000 + @thread_length_default 2_000 + + def run(args) do + start_pleroma() + Pleroma.Config.put([:instance, :skip_thread_containment], true) + {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) + + users_max = Keyword.get(opts, :users, @users_default) + dms_max = Keyword.get(opts, :dms, @dms_default) + thread_length = Keyword.get(opts, :thread_length, @thread_length_default) + + clean_tables() + + opts = + Keyword.put(opts, :users_max, users_max) + |> Keyword.put(:dms_max, dms_max) + |> Keyword.put(:thread_length, thread_length) + + generate_users(opts) + + # main user for queries + IO.puts("Fetching local main user...") + + {time, user} = + :timer.tc(fn -> + Repo.one( + from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1) + ) + end) + + IO.puts("Fetching main user take #{to_sec(time)} sec.\n") + + IO.puts("Fetching local users...") + + {time, users} = + :timer.tc(fn -> + Repo.all( + from(u in User, + where: u.id != ^user.id, + where: u.local == true, + order_by: fragment("RANDOM()"), + limit: 10 + ) + ) + end) + + IO.puts("Fetching local users take #{to_sec(time)} sec.\n") + + IO.puts("Fetching remote users...") + + {time, remote_users} = + :timer.tc(fn -> + Repo.all( + from(u in User, + where: u.id != ^user.id, + where: u.local == false, + order_by: fragment("RANDOM()"), + limit: 10 + ) + ) + end) + + IO.puts("Fetching remote users take #{to_sec(time)} sec.\n") + + generate_activities(user, users) + + generate_remote_activities(user, remote_users) + + generate_dms(user, users, opts) + + {:ok, activity} = generate_long_thread(user, users, opts) + + generate_non_visible_message(user, users) + + IO.puts("Users in DB: #{Repo.aggregate(from(u in User), :count, :id)}") + + IO.puts("Activities in DB: #{Repo.aggregate(from(a in Pleroma.Activity), :count, :id)}") + + IO.puts("Objects in DB: #{Repo.aggregate(from(o in Pleroma.Object), :count, :id)}") + + IO.puts( + "Notifications in DB: #{Repo.aggregate(from(n in Pleroma.Notification), :count, :id)}" + ) + + fetch_user(user) + query_timelines(user) + query_notifications(user) + query_dms(user) + query_long_thread(user, activity) + Pleroma.Config.put([:instance, :skip_thread_containment], false) + query_timelines(user) + end + + defp clean_tables do + IO.puts("Deleting old data...\n") + Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;") + Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;") + Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;") + end +end diff --git a/config/benchmark.exs b/config/benchmark.exs new file mode 100644 index 000000000..dd99cf5fd --- /dev/null +++ b/config/benchmark.exs @@ -0,0 +1,84 @@ +use Mix.Config + +# We don't run a server during test. If one is required, +# you can enable the server option below. +config :pleroma, Pleroma.Web.Endpoint, + http: [port: 4001], + url: [port: 4001], + server: true + +# Disable captha for tests +config :pleroma, Pleroma.Captcha, + # It should not be enabled for automatic tests + enabled: false, + # A fake captcha service for tests + method: Pleroma.Captcha.Mock + +# Print only warnings and errors during test +config :logger, level: :warn + +config :pleroma, :auth, oauth_consumer_strategies: [] + +config :pleroma, Pleroma.Upload, filters: [], link_name: false + +config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads" + +config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test, enabled: true + +config :pleroma, :instance, + email: "admin@example.com", + notify_email: "noreply@example.com", + skip_thread_containment: false, + federating: false, + external_user_synchronization: false + +config :pleroma, :activitypub, sign_object_fetches: false + +# Configure your database +config :pleroma, Pleroma.Repo, + adapter: Ecto.Adapters.Postgres, + username: "postgres", + password: "postgres", + database: "pleroma_test", + hostname: System.get_env("DB_HOST") || "localhost", + pool_size: 10 + +# Reduce hash rounds for testing +config :pbkdf2_elixir, rounds: 1 + +config :tesla, adapter: Tesla.Mock + +config :pleroma, :rich_media, + enabled: false, + ignore_hosts: [], + ignore_tld: ["local", "localdomain", "lan"] + +config :web_push_encryption, :vapid_details, + subject: "mailto:administrator@example.com", + public_key: + "BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4", + private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA" + +config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock + +config :pleroma_job_queue, disabled: true + +config :pleroma, Pleroma.ScheduledActivity, + daily_user_limit: 2, + total_user_limit: 3, + enabled: false + +config :pleroma, :rate_limit, + search: [{1000, 30}, {1000, 30}], + app_account_creation: {10_000, 5}, + password_reset: {1000, 30} + +config :pleroma, :http_security, report_uri: "https://endpoint.com" + +config :pleroma, :http, send_user_agent: false + +rum_enabled = System.get_env("RUM_ENABLED") == "true" +config :pleroma, :database, rum_enabled: rum_enabled +IO.puts("RUM enabled: #{rum_enabled}") + +config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock diff --git a/config/config.exs b/config/config.exs index f4d92102f..787809b27 100644 --- a/config/config.exs +++ b/config/config.exs @@ -59,10 +59,6 @@ _ -> [] end -scheduled_jobs = - scheduled_jobs ++ - [{"0 */6 * * * *", {Pleroma.Web.Websub, :refresh_subscriptions, []}}] - config :pleroma, Pleroma.Scheduler, global: true, overlap: true, @@ -243,9 +239,7 @@ federation_incoming_replies_max_depth: 100, federation_reachability_timeout_days: 7, federation_publisher_modules: [ - Pleroma.Web.ActivityPub.Publisher, - Pleroma.Web.Websub, - Pleroma.Web.Salmon + Pleroma.Web.ActivityPub.Publisher ], allow_relay: true, rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, @@ -290,8 +284,8 @@ allow_tables: false, allow_fonts: false, scrub_policy: [ - Pleroma.HTML.Transform.MediaProxy, - Pleroma.HTML.Scrubber.Default + Pleroma.HTML.Scrubber.Default, + Pleroma.HTML.Transform.MediaProxy ] config :pleroma, :frontend_configurations, @@ -328,6 +322,16 @@ ], default_mascot: :pleroma_fox_tan +config :pleroma, :manifest, + icons: [ + %{ + src: "/static/logo.png", + type: "image/png" + } + ], + theme_color: "#282c37", + background_color: "#191b22" + config :pleroma, :activitypub, unfollow_blocked: true, outgoing_blocks: true, @@ -599,6 +603,7 @@ activity_pub: nil, activity_pub_question: 30_000 +config :swarm, node_blacklist: [~r/myhtml_.*$/] # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/config/description.exs b/config/description.exs index b007cf69c..70e963399 100644 --- a/config/description.exs +++ b/config/description.exs @@ -581,9 +581,7 @@ type: [:list, :module], description: "List of modules for federation publishing", suggestions: [ - Pleroma.Web.ActivityPub.Publisher, - Pleroma.Web.Websub, - Pleroma.Web.Salmo + Pleroma.Web.ActivityPub.Publisher ] }, %{ @@ -1100,6 +1098,45 @@ } ] }, + %{ + group: :pleroma, + key: :manifest, + type: :group, + description: + "This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE", + children: [ + %{ + key: :icons, + type: {:list, :map}, + description: "Describe the icons of the app", + suggestion: [ + %{ + src: "/static/logo.png" + }, + %{ + src: "/static/icon.png", + type: "image/png" + }, + %{ + src: "/static/icon.ico", + sizes: "72x72 96x96 128x128 256x256" + } + ] + }, + %{ + key: :theme_color, + type: :string, + description: "Describe the theme color of the app", + suggestions: ["#282c37", "mediumpurple"] + }, + %{ + key: :background_color, + type: :string, + description: "Describe the background color of the app", + suggestions: ["#191b22", "aliceblue"] + } + ] + }, %{ group: :pleroma, key: :mrf_simple, diff --git a/config/releases.exs b/config/releases.exs index 98c5ceccd..36c493673 100644 --- a/config/releases.exs +++ b/config/releases.exs @@ -1,6 +1,6 @@ import Config -config :pleroma, :instance, static_dir: "/var/lib/pleroma/static" +config :pleroma, :instance, static: "/var/lib/pleroma/static" config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads" config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs" diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md index ee9e68cb1..c042b08ac 100644 --- a/docs/API/admin_api.md +++ b/docs/API/admin_api.md @@ -47,7 +47,7 @@ Authentication is required and the user must be an admin. } ``` -## `/api/pleroma/admin/users` +## DEPRECATED `DELETE /api/pleroma/admin/users` ### Remove a user @@ -56,6 +56,15 @@ Authentication is required and the user must be an admin. - `nickname` - Response: User’s nickname +## `DELETE /api/pleroma/admin/users` + +### Remove a user + +- Method `DELETE` +- Params: + - `nicknames` +- Response: Array of user nicknames + ### Create a user - Method: `POST` @@ -154,28 +163,86 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret } ``` -### Add user in permission group +## DEPRECATED `POST /api/pleroma/admin/users/:nickname/permission_group/:permission_group` + +### Add user to permission group -- Method: `POST` - Params: none - Response: - On failure: `{"error": "…"}` - - On success: JSON of the `user.info` + - On success: JSON of the user + +## `POST /api/pleroma/admin/users/permission_group/:permission_group` + +### Add users to permission group + +- Params: + - `nicknames`: nicknames array +- Response: + - On failure: `{"error": "…"}` + - On success: JSON of the user + +## DEPRECATED `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` ### Remove user from permission group -- Method: `DELETE` - Params: none - Response: - On failure: `{"error": "…"}` - - On success: JSON of the `user.info` + - On success: JSON of the user - Note: An admin cannot revoke their own admin status. -## `/api/pleroma/admin/users/:nickname/activation_status` +## `DELETE /api/pleroma/admin/users/permission_group/:permission_group` + +### Remove users from permission group + +- Params: + - `nicknames`: nicknames array +- Response: + - On failure: `{"error": "…"}` + - On success: JSON of the user +- Note: An admin cannot revoke their own admin status. + +## `PATCH /api/pleroma/admin/users/activate` + +### Activate user + +- Params: + - `nicknames`: nicknames array +- Response: + +```json +{ + users: [ + { + // user object + } + ] +} +``` + +## `PATCH /api/pleroma/admin/users/deactivate` + +### Deactivate user + +- Params: + - `nicknames`: nicknames array +- Response: + +```json +{ + users: [ + { + // user object + } + ] +} +``` + +## DEPRECATED `PATCH /api/pleroma/admin/users/:nickname/activation_status` ### Active or deactivate a user -- Method: `PUT` - Params: - `nickname` - `status` BOOLEAN field, false value means deactivation. @@ -222,6 +289,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - Response: - On success: URL of the unfollowed relay +## `GET /api/pleroma/admin/relay` + +### List Relays + +- Params: none +- Response: + - On success: JSON array of relays + ## `/api/pleroma/admin/users/invite_token` ### Create an account registration invite token @@ -317,13 +392,13 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret } ``` - -## `/api/pleroma/admin/users/:nickname/force_password_reset` +## `/api/pleroma/admin/users/force_password_reset` ### Force passord reset for a user with a given nickname - Methods: `PATCH` -- Params: none +- Params: + - `nicknames` - Response: none (code `204`) ## `/api/pleroma/admin/reports` diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index 21b297529..7fbe17130 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -13,6 +13,7 @@ Some apps operate under the assumption that no more than 4 attachments can be re ## Timelines Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users. +Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`. ## Statuses @@ -71,6 +72,12 @@ Has an additional field under the `pleroma` object: - `recipients`: The list of the recipients of this Conversation. These will be addressed when replying to this conversation. +## GET `/api/v1/conversations` + +Accepts additional parameters: + +- `recipients`: Only return conversations with the given recipients (a list of user ids). Usage example: `GET /api/v1/conversations?recipients[]=1&recipients[]=2` + ## Account Search Behavior has changed: @@ -84,6 +91,12 @@ Has these additional fields under the `pleroma` object: - `is_seen`: true if the notification was read by the user +## 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`. + ## POST `/api/v1/statuses` Additional parameters can be added to the JSON body/Form data: diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 38ccd5377..ad16d027e 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -367,6 +367,13 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though. * Response: JSON, statuses (200 - healthy, 503 unhealthy) +## `GET /api/v1/pleroma/conversations/read` +### Marks all user's conversations as read. +* Method `POST` +* Authentication: required +* Params: None +* Response: JSON, returns a list of Mastodon Conversation entities that were marked as read (200 - healthy, 503 unhealthy). + ## `GET /api/pleroma/emoji/packs` ### Lists the custom emoji packs on the server * Method `GET` diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 8d85276ed..3427ae419 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -247,6 +247,35 @@ relates to mascots on the mastodon frontend * `default_mascot`: An element from `mascots` - This will be used as the default mascot on MastoFE (default: `:pleroma_fox_tan`) +## :manifest + +This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE. + +* `icons`: Describe the icons of the app, this a list of maps describing icons in the same way as the + [spec](https://www.w3.org/TR/appmanifest/#imageresource-and-its-members) describes it. + + Example: + + ```elixir + config :pleroma, :manifest, + icons: [ + %{ + src: "/static/logo.png" + }, + %{ + src: "/static/icon.png", + type: "image/png" + }, + %{ + src: "/static/icon.ico", + sizes: "72x72 96x96 128x128 256x256" + } + ] + ``` + +* `theme_color`: Describe the theme color of the app. (Example: `"#282c37"`, `"rebeccapurple"`) +* `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`) + ## :mrf_simple * `media_removal`: List of instances to remove medias from * `media_nsfw`: List of instances to put medias as NSFW(sensitive) from diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index f5d1fade1..2a9b8f6ff 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -91,7 +91,7 @@ sudo adduser -S -s /bin/false -h /opt/pleroma -H -G pleroma pleroma ```shell sudo mkdir -p /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma -sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma +sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma ``` * Change to the new directory: diff --git a/docs/installation/arch_linux_en.md b/docs/installation/arch_linux_en.md index 58a8d023f..8370986ad 100644 --- a/docs/installation/arch_linux_en.md +++ b/docs/installation/arch_linux_en.md @@ -66,7 +66,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma ```shell sudo mkdir -p /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma -sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma +sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma ``` * Change to the new directory: diff --git a/docs/installation/centos7_en.md b/docs/installation/centos7_en.md index fe26339b5..ad4f58dc1 100644 --- a/docs/installation/centos7_en.md +++ b/docs/installation/centos7_en.md @@ -143,7 +143,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma ```shell sudo mkdir -p /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma -sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma +sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma ``` * Change to the new directory: diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index e5f8dd670..fe2dbb92d 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma ```shell sudo mkdir -p /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma -sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma +sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma ``` * Change to the new directory: diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index e7d834b17..7aa0bcc24 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma ``` sudo mkdir -p /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma -sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma +sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma ``` * 新しいディレクトリに移動します。 diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md index 55d677acf..1e61373cc 100644 --- a/docs/installation/gentoo_en.md +++ b/docs/installation/gentoo_en.md @@ -106,7 +106,7 @@ It is highly recommended you use your own fork for the `https://path/to/repo` pa ```shell pleroma$ cd ~ - pleroma$ git clone -b master https://path/to/repo + pleroma$ git clone -b stable https://path/to/repo ``` * Change to the new directory: diff --git a/docs/installation/migrating_from_source_otp_en.md b/docs/installation/migrating_from_source_otp_en.md index e204cb3ee..87568faad 100644 --- a/docs/installation/migrating_from_source_otp_en.md +++ b/docs/installation/migrating_from_source_otp_en.md @@ -96,9 +96,9 @@ rm -r ~pleroma/* export FLAVOUR="arm64-musl" # Clone the release build into a temporary directory and unpack it -# Replace `master` with `develop` if you want to run the develop branch +# Replace `stable` with `unstable` if you want to run the unstable branch su pleroma -s $SHELL -lc " -curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/master/download?job=$FLAVOUR' -o /tmp/pleroma.zip +curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip unzip /tmp/pleroma.zip -d /tmp/ " diff --git a/docs/installation/netbsd_en.md b/docs/installation/netbsd_en.md index a096d5354..6a922a27e 100644 --- a/docs/installation/netbsd_en.md +++ b/docs/installation/netbsd_en.md @@ -58,7 +58,7 @@ Clone the repository: ``` $ cd /home/pleroma -$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git +$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git ``` Configure Pleroma. Note that you need a domain name at this point: diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md index fcba38b2c..3585a326b 100644 --- a/docs/installation/openbsd_en.md +++ b/docs/installation/openbsd_en.md @@ -29,7 +29,7 @@ This creates a "pleroma" login class and sets higher values than default for dat Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): `useradd -m -L pleroma _pleroma` #### Clone pleroma's directory -Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b master https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide. +Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide. #### Postgresql Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql: diff --git a/docs/installation/openbsd_fi.md b/docs/installation/openbsd_fi.md index 39819a8c8..272273cff 100644 --- a/docs/installation/openbsd_fi.md +++ b/docs/installation/openbsd_fi.md @@ -44,7 +44,7 @@ Vaihda pleroma-käyttäjään ja mene kotihakemistoosi: Lataa pleroman lähdekoodi: -`$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git` +`$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git` `$ cd pleroma` diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index b4e5254cd..c028f4229 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -80,7 +80,7 @@ export FLAVOUR="arm64-musl" # Clone the release build into a temporary directory and unpack it su pleroma -s $SHELL -lc " -curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/master/download?job=$FLAVOUR' -o /tmp/pleroma.zip +curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip unzip /tmp/pleroma.zip -d /tmp/ " diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index cfd9eeada..e2b5251bc 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -28,7 +28,7 @@ def run(["remove_embedded_objects" | args]) do Logger.info("Removing embedded objects") Repo.query!( - "update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;", + "update activities set data = safe_jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;", [], timeout: :infinity ) @@ -52,9 +52,9 @@ def run(["bump_all_conversations"]) do def run(["update_users_following_followers_counts"]) do start_pleroma() - users = Repo.all(User) - Enum.each(users, &User.remove_duplicated_following/1) - Enum.each(users, &User.update_follower_count/1) + User + |> Repo.all() + |> Enum.each(&User.update_follower_count/1) end def run(["prune_objects" | args]) do @@ -126,7 +126,7 @@ def run(["fix_likes_collections"]) do set: [ data: fragment( - "jsonb_set(?, '{likes}', '[]'::jsonb, true)", + "safe_jsonb_set(?, '{likes}', '[]'::jsonb, true)", object.data ) ] diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index 6ef0a635d..35669af27 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -111,19 +111,21 @@ def run(["get-packs" | args]) do file_list: files_to_unzip ) - IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name])) + IO.puts(IO.ANSI.format(["Writing pack.json for ", :bright, pack_name])) - emoji_txt_str = - Enum.map( - files, - fn {shortcode, path} -> - emojo_path = Path.join("/emoji/#{pack_name}", path) - "#{shortcode}, #{emojo_path}" - end - ) - |> Enum.join("\n") + pack_json = %{ + pack: %{ + "license" => pack["license"], + "homepage" => pack["homepage"], + "description" => pack["description"], + "fallback-src" => pack["src"], + "fallback-src-sha256" => pack["src_sha256"], + "share-files" => true + }, + files: files + } - File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str) + File.write!(Path.join(pack_path, "pack.json"), Jason.encode!(pack_json, pretty: true)) else IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"])) end diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex index d7a7b599f..7ef5f9678 100644 --- a/lib/mix/tasks/pleroma/relay.ex +++ b/lib/mix/tasks/pleroma/relay.ex @@ -5,7 +5,6 @@ defmodule Mix.Tasks.Pleroma.Relay do use Mix.Task import Mix.Pleroma - alias Pleroma.User alias Pleroma.Web.ActivityPub.Relay @shortdoc "Manages remote relays" @@ -36,13 +35,10 @@ def run(["unfollow", target]) do def run(["list"]) do start_pleroma() - with %User{following: following} = _user <- Relay.get_actor() do - following - |> Enum.map(fn entry -> URI.parse(entry).host end) - |> Enum.uniq() - |> Enum.each(&shell_info(&1)) + with {:ok, list} <- Relay.list() do + list |> Enum.each(&shell_info(&1)) else - e -> shell_error("Error while fetching relay subscription list: #{inspect(e)}") + {:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}") end end end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 134b5bccc..4e3b80db3 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -5,6 +5,7 @@ defmodule Mix.Tasks.Pleroma.User do use Mix.Task import Mix.Pleroma + alias Ecto.Changeset alias Pleroma.User alias Pleroma.UserInviteToken alias Pleroma.Web.OAuth @@ -109,10 +110,10 @@ def run(["toggle_activated", nickname]) do start_pleroma() with %User{} = user <- User.get_cached_by_nickname(nickname) do - {:ok, user} = User.deactivate(user, !user.info.deactivated) + {:ok, user} = User.deactivate(user, !user.deactivated) shell_info( - "Activation status of #{nickname}: #{if(user.info.deactivated, do: "de", else: "")}activated" + "Activation status of #{nickname}: #{if(user.deactivated, do: "de", else: "")}activated" ) else _ -> @@ -162,7 +163,7 @@ def run(["unsubscribe", nickname]) do user = User.get_cached_by_id(user.id) - if Enum.empty?(user.following) do + if Enum.empty?(User.get_friends(user)) do shell_info("Successfully unsubscribed all followers from #{user.nickname}") end else @@ -340,7 +341,7 @@ def run(["toggle_confirmed", nickname]) do with %User{} = user <- User.get_cached_by_nickname(nickname) do {:ok, user} = User.toggle_confirmation(user) - message = if user.info.confirmation_pending, do: "needs", else: "doesn't need" + message = if user.confirmation_pending, do: "needs", else: "doesn't need" shell_info("#{nickname} #{message} confirmation.") else @@ -364,23 +365,32 @@ def run(["sign_out", nickname]) do end defp set_moderator(user, value) do - {:ok, user} = User.update_info(user, &User.Info.admin_api_update(&1, %{is_moderator: value})) + {:ok, user} = + user + |> Changeset.change(%{is_moderator: value}) + |> User.update_and_set_cache() - shell_info("Moderator status of #{user.nickname}: #{user.info.is_moderator}") + shell_info("Moderator status of #{user.nickname}: #{user.is_moderator}") user end defp set_admin(user, value) do - {:ok, user} = User.update_info(user, &User.Info.admin_api_update(&1, %{is_admin: value})) + {:ok, user} = + user + |> Changeset.change(%{is_admin: value}) + |> User.update_and_set_cache() - shell_info("Admin status of #{user.nickname}: #{user.info.is_admin}") + shell_info("Admin status of #{user.nickname}: #{user.is_admin}") user end defp set_locked(user, value) do - {:ok, user} = User.update_info(user, &User.Info.user_upgrade(&1, %{locked: value})) + {:ok, user} = + user + |> Changeset.change(%{locked: value}) + |> User.update_and_set_cache() - shell_info("Locked status of #{user.nickname}: #{user.info.locked}") + shell_info("Locked status of #{user.nickname}: #{user.locked}") user end end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 0bf218bc7..d681eecc8 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -161,11 +161,6 @@ defp task_children(:test) do id: :web_push_init, start: {Task, :start_link, [&Pleroma.Web.Push.init/0]}, restart: :temporary - }, - %{ - id: :federator_init, - start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]}, - restart: :temporary } ] end @@ -177,11 +172,6 @@ defp task_children(_) do start: {Task, :start_link, [&Pleroma.Web.Push.init/0]}, restart: :temporary }, - %{ - id: :federator_init, - start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]}, - restart: :temporary - }, %{ id: :internal_fetch_init, start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]}, diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex index fa838a4e4..054d422b0 100644 --- a/lib/pleroma/bbs/handler.ex +++ b/lib/pleroma/bbs/handler.ex @@ -5,6 +5,7 @@ defmodule Pleroma.BBS.Handler do use Sshd.ShellHandler alias Pleroma.Activity + alias Pleroma.HTML alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI @@ -44,7 +45,7 @@ defp loop(state) do def puts_activity(activity) do status = Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{activity: activity}) IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})") - IO.puts(HtmlSanitizeEx.strip_tags(status.content)) + IO.puts(HTML.strip_tags(status.content)) IO.puts("") end @@ -97,7 +98,7 @@ def handle_command(state, "home") do |> Map.put("user", user) activities = - [user.ap_id | user.following] + [user.ap_id | Pleroma.User.following(user)] |> ActivityPub.fetch_activities(params) Enum.each(activities, fn activity -> diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex index 098016af2..ade3a526a 100644 --- a/lib/pleroma/conversation.ex +++ b/lib/pleroma/conversation.ex @@ -67,7 +67,13 @@ def create_or_bump_for(activity, opts \\ []) do participations = Enum.map(users, fn user -> - User.increment_unread_conversation_count(conversation, user) + invisible_conversation = Enum.any?(users, &User.blocks?(user, &1)) + + unless invisible_conversation do + User.increment_unread_conversation_count(conversation, user) + end + + opts = Keyword.put(opts, :invisible_conversation, invisible_conversation) {:ok, participation} = Participation.create_for_user_and_conversation(user, conversation, opts) diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index ab81f3217..aafe57280 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -32,11 +32,20 @@ def creation_cng(struct, params) do def create_for_user_and_conversation(user, conversation, opts \\ []) do read = !!opts[:read] + invisible_conversation = !!opts[:invisible_conversation] + + update_on_conflict = + if(invisible_conversation, do: [], else: [read: read]) + |> Keyword.put(:updated_at, NaiveDateTime.utc_now()) %__MODULE__{} - |> creation_cng(%{user_id: user.id, conversation_id: conversation.id, read: read}) + |> creation_cng(%{ + user_id: user.id, + conversation_id: conversation.id, + read: invisible_conversation || read + }) |> Repo.insert( - on_conflict: [set: [read: read, updated_at: NaiveDateTime.utc_now()]], + on_conflict: [set: update_on_conflict], returning: true, conflict_target: [:user_id, :conversation_id] ) @@ -48,6 +57,12 @@ def read_cng(struct, params) do |> validate_required([:read]) end + def mark_as_read(%User{} = user, %Conversation{} = conversation) do + with %__MODULE__{} = participation <- for_user_and_conversation(user, conversation) do + mark_as_read(participation) + end + end + def mark_as_read(participation) do participation |> read_cng(%{read: true}) @@ -63,6 +78,38 @@ def mark_as_read(participation) do end end + def mark_all_as_read(%User{local: true} = user, %User{} = target_user) do + target_conversation_ids = + __MODULE__ + |> where([p], p.user_id == ^target_user.id) + |> select([p], p.conversation_id) + |> Repo.all() + + __MODULE__ + |> where([p], p.user_id == ^user.id) + |> where([p], p.conversation_id in ^target_conversation_ids) + |> update([p], set: [read: true]) + |> Repo.update_all([]) + + {:ok, user} = User.set_unread_conversation_count(user) + {:ok, user, []} + end + + def mark_all_as_read(%User{} = user, %User{}), do: {:ok, user, []} + + def mark_all_as_read(%User{} = user) do + {_, participations} = + __MODULE__ + |> where([p], p.user_id == ^user.id) + |> where([p], not p.read) + |> update([p], set: [read: true]) + |> select([p], p) + |> Repo.update_all([]) + + {:ok, user} = User.set_unread_conversation_count(user) + {:ok, user, participations} + end + def mark_as_unread(participation) do participation |> read_cng(%{read: false}) @@ -75,9 +122,37 @@ def for_user(user, params \\ %{}) do order_by: [desc: p.updated_at], preload: [conversation: [:users]] ) + |> restrict_recipients(user, params) |> Pleroma.Pagination.fetch_paginated(params) end + def restrict_recipients(query, user, %{"recipients" => user_ids}) do + user_ids = + [user.id | user_ids] + |> Enum.uniq() + |> Enum.reduce([], fn user_id, acc -> + case FlakeId.Ecto.CompatType.dump(user_id) do + {:ok, user_id} -> [user_id | acc] + _ -> acc + end + end) + + conversation_subquery = + __MODULE__ + |> group_by([p], p.conversation_id) + |> having( + [p], + count(p.user_id) == ^length(user_ids) and + fragment("array_agg(?) @> ?", p.user_id, ^user_ids) + ) + |> select([p], %{id: p.conversation_id}) + + query + |> join(:inner, [p], c in subquery(conversation_subquery), on: p.conversation_id == c.id) + end + + def restrict_recipients(query, _, _), do: query + def for_user_and_conversation(user, conversation) do from(p in __MODULE__, where: p.user_id == ^user.id, diff --git a/lib/pleroma/daemons/digest_email_daemon.ex b/lib/pleroma/daemons/digest_email_daemon.ex index 462ad2c55..b4c8eaad9 100644 --- a/lib/pleroma/daemons/digest_email_daemon.ex +++ b/lib/pleroma/daemons/digest_email_daemon.ex @@ -17,7 +17,7 @@ def perform do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) from(u in inactive_users_query, - where: fragment(~s(? #> '{"email_notifications","digest"}' @> 'true'), u.info), + where: fragment(~s(? ->'digest' @> 'true'), u.email_notifications), where: u.last_digest_emailed_at < datetime_add(^now, ^negative_interval, "day"), select: u ) diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index 40b67ff56..a10f88f93 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -72,7 +72,7 @@ def account_confirmation_email(user) do Endpoint, :confirm_email, user.id, - to_string(user.info.confirmation_token) + to_string(user.confirmation_token) ) html_body = """ diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex new file mode 100644 index 000000000..2ffac17ee --- /dev/null +++ b/lib/pleroma/following_relationship.ex @@ -0,0 +1,110 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.FollowingRelationship do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias FlakeId.Ecto.CompatType + alias Pleroma.Repo + alias Pleroma.User + + schema "following_relationships" do + field(:state, :string, default: "accept") + + belongs_to(:follower, User, type: CompatType) + belongs_to(:following, User, type: CompatType) + + timestamps() + end + + def changeset(%__MODULE__{} = following_relationship, attrs) do + following_relationship + |> cast(attrs, [:state]) + |> put_assoc(:follower, attrs.follower) + |> put_assoc(:following, attrs.following) + |> validate_required([:state, :follower, :following]) + end + + def get(%User{} = follower, %User{} = following) do + __MODULE__ + |> where(follower_id: ^follower.id, following_id: ^following.id) + |> Repo.one() + end + + def update(follower, following, "reject"), do: unfollow(follower, following) + + def update(%User{} = follower, %User{} = following, state) do + case get(follower, following) do + nil -> + follow(follower, following, state) + + following_relationship -> + following_relationship + |> cast(%{state: state}, [:state]) + |> validate_required([:state]) + |> Repo.update() + end + end + + def follow(%User{} = follower, %User{} = following, state \\ "accept") do + %__MODULE__{} + |> changeset(%{follower: follower, following: following, state: state}) + |> Repo.insert(on_conflict: :nothing) + end + + def unfollow(%User{} = follower, %User{} = following) do + case get(follower, following) do + nil -> {:ok, nil} + %__MODULE__{} = following_relationship -> Repo.delete(following_relationship) + end + end + + def follower_count(%User{} = user) do + %{followers: user, deactivated: false} + |> User.Query.build() + |> Repo.aggregate(:count, :id) + end + + def following_count(%User{id: nil}), do: 0 + + def following_count(%User{} = user) do + %{friends: user, deactivated: false} + |> User.Query.build() + |> Repo.aggregate(:count, :id) + end + + def get_follow_requests(%User{id: id}) do + __MODULE__ + |> join(:inner, [r], f in assoc(r, :follower)) + |> where([r], r.state == "pending") + |> where([r], r.following_id == ^id) + |> select([r, f], f) + |> Repo.all() + end + + def following?(%User{id: follower_id}, %User{id: followed_id}) do + __MODULE__ + |> where(follower_id: ^follower_id, following_id: ^followed_id, state: "accept") + |> Repo.exists?() + end + + def following(%User{} = user) do + following = + __MODULE__ + |> join(:inner, [r], u in User, on: r.following_id == u.id) + |> where([r], r.follower_id == ^user.id) + |> where([r], r.state == "accept") + |> select([r, u], u.follower_address) + |> Repo.all() + + if not user.local or user.nickname in [nil, "internal.fetch"] do + following + else + [user.follower_address | following] + end + end +end diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 931b9af2b..19b9af46c 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -127,7 +127,7 @@ def truncate(text, max_length \\ 200, omission \\ "...") do end end - defp get_ap_id(%User{info: %{source_data: %{"url" => url}}}) when is_binary(url), do: url + defp get_ap_id(%User{source_data: %{"url" => url}}) when is_binary(url), do: url defp get_ap_id(%User{ap_id: ap_id}), do: ap_id defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname) diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 937bafed5..997e965f0 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -3,8 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTML do - alias HtmlSanitizeEx.Scrubber - 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] @@ -24,9 +22,13 @@ def filter_tags(html, scrubbers) when is_list(scrubbers) do end) end - def filter_tags(html, scrubber), do: Scrubber.scrub(html, scrubber) + def filter_tags(html, scrubber) do + {:ok, content} = FastSanitize.Sanitizer.scrub(html, scrubber) + content + end + def filter_tags(html), do: filter_tags(html, nil) - def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags) + def strip_tags(html), do: filter_tags(html, FastSanitize.Sanitizer.StripTags) def get_cached_scrubbed_html_for_activity( content, @@ -46,7 +48,7 @@ def get_cached_scrubbed_html_for_activity( def get_cached_stripped_html_for_activity(content, activity, key) do get_cached_scrubbed_html_for_activity( content, - HtmlSanitizeEx.Scrubber.StripTags, + FastSanitize.Sanitizer.StripTags, activity, key, &HtmlEntities.decode/1 @@ -106,16 +108,15 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) - require HtmlSanitizeEx.Scrubber.Meta - alias HtmlSanitizeEx.Scrubber.Meta + require FastSanitize.Sanitizer.Meta + alias FastSanitize.Sanitizer.Meta - Meta.remove_cdata_sections_before_scrub() Meta.strip_comments() # links - Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) + Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) - Meta.allow_tag_with_this_attribute_values("a", "class", [ + Meta.allow_tag_with_this_attribute_values(:a, "class", [ "hashtag", "u-url", "mention", @@ -123,29 +124,29 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do "mention u-url" ]) - Meta.allow_tag_with_this_attribute_values("a", "rel", [ + Meta.allow_tag_with_this_attribute_values(:a, "rel", [ "tag", "nofollow", "noopener", "noreferrer" ]) - Meta.allow_tag_with_these_attributes("a", ["name", "title"]) + 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", []) + 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", []) + 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_uri_attributes(:img, ["src"], ["http", "https"]) - Meta.allow_tag_with_these_attributes("img", [ + Meta.allow_tag_with_these_attributes(:img, [ "width", "height", "class", @@ -160,19 +161,18 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do defmodule Pleroma.HTML.Scrubber.Default do @doc "The default HTML scrubbing policy: no " - require HtmlSanitizeEx.Scrubber.Meta - alias HtmlSanitizeEx.Scrubber.Meta + 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.remove_cdata_sections_before_scrub() Meta.strip_comments() - Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) + Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) - Meta.allow_tag_with_this_attribute_values("a", "class", [ + Meta.allow_tag_with_this_attribute_values(:a, "class", [ "hashtag", "u-url", "mention", @@ -180,7 +180,7 @@ defmodule Pleroma.HTML.Scrubber.Default do "mention u-url" ]) - Meta.allow_tag_with_this_attribute_values("a", "rel", [ + Meta.allow_tag_with_this_attribute_values(:a, "rel", [ "tag", "nofollow", "noopener", @@ -188,37 +188,37 @@ defmodule Pleroma.HTML.Scrubber.Default do "ugc" ]) - Meta.allow_tag_with_these_attributes("a", ["name", "title"]) + Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) - Meta.allow_tag_with_these_attributes("abbr", ["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_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", []) + 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_uri_attributes(:img, ["src"], ["http", "https"]) - Meta.allow_tag_with_these_attributes("img", [ + Meta.allow_tag_with_these_attributes(:img, [ "width", "height", "class", @@ -228,24 +228,24 @@ defmodule Pleroma.HTML.Scrubber.Default do 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", []) + 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", []) + 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"]) + Meta.allow_tag_with_these_attributes(:font, ["face"]) end Meta.strip_everything_not_covered() @@ -258,7 +258,7 @@ defmodule Pleroma.HTML.Transform.MediaProxy do def before_scrub(html), do: html - def scrub_attribute("img", {"src", "http" <> target}) do + def scrub_attribute(:img, {"src", "http" <> target}) do media_url = ("http" <> target) |> MediaProxy.url() @@ -268,16 +268,16 @@ def scrub_attribute("img", {"src", "http" <> target}) do def scrub_attribute(_tag, attribute), do: attribute - def scrub({"img", attributes, children}) do + def scrub({:img, attributes, children}) do attributes = attributes - |> Enum.map(fn attr -> scrub_attribute("img", attr) end) + |> Enum.map(fn attr -> scrub_attribute(:img, attr) end) |> Enum.reject(&is_nil(&1)) - {"img", attributes, children} + {:img, attributes, children} end - def scrub({:comment, _children}), do: "" + def scrub({:comment, _text, _children}), do: "" def scrub({tag, attributes, children}), do: {tag, attributes, children} def scrub({_tag, children}), do: children @@ -291,16 +291,15 @@ defmodule Pleroma.HTML.Scrubber.LinksOnly do @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) - require HtmlSanitizeEx.Scrubber.Meta - alias HtmlSanitizeEx.Scrubber.Meta + require FastSanitize.Sanitizer.Meta + alias FastSanitize.Sanitizer.Meta - Meta.remove_cdata_sections_before_scrub() Meta.strip_comments() # links - Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes) + Meta.allow_tag_with_uri_attributes(:a, ["href"], @valid_schemes) - Meta.allow_tag_with_this_attribute_values("a", "rel", [ + Meta.allow_tag_with_this_attribute_values(:a, "rel", [ "tag", "nofollow", "noopener", @@ -309,6 +308,6 @@ defmodule Pleroma.HTML.Scrubber.LinksOnly do "ugc" ]) - Meta.allow_tag_with_these_attributes("a", ["name", "title"]) + Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) Meta.strip_everything_not_covered() end diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex new file mode 100644 index 000000000..7f87c86c3 --- /dev/null +++ b/lib/pleroma/marker.ex @@ -0,0 +1,74 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Marker do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Ecto.Multi + alias Pleroma.Repo + alias Pleroma.User + + @timelines ["notifications"] + + schema "markers" do + field(:last_read_id, :string, default: "") + field(:timeline, :string, default: "") + field(:lock_version, :integer, default: 0) + + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + timestamps() + end + + def get_markers(user, timelines \\ []) do + Repo.all(get_query(user, timelines)) + end + + def upsert(%User{} = user, attrs) do + attrs + |> Map.take(@timelines) + |> Enum.reduce(Multi.new(), fn {timeline, timeline_attrs}, multi -> + marker = + user + |> get_marker(timeline) + |> changeset(timeline_attrs) + + Multi.insert(multi, timeline, marker, + returning: true, + on_conflict: {:replace, [:last_read_id]}, + conflict_target: [:user_id, :timeline] + ) + end) + |> Repo.transaction() + end + + defp get_marker(user, timeline) do + case Repo.find_resource(get_query(user, timeline)) do + {:ok, marker} -> %__MODULE__{marker | user: user} + _ -> %__MODULE__{timeline: timeline, user_id: user.id} + end + end + + @doc false + defp changeset(marker, attrs) do + marker + |> cast(attrs, [:last_read_id]) + |> validate_required([:user_id, :timeline, :last_read_id]) + |> validate_inclusion(:timeline, @timelines) + end + + defp by_timeline(query, timeline) do + from(m in query, where: m.timeline in ^List.wrap(timeline)) + end + + defp by_user_id(query, id), do: from(m in query, where: m.user_id == ^id) + + defp get_query(user, timelines) do + __MODULE__ + |> by_user_id(user.id) + |> by_timeline(timelines) + end +end diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 352cad433..ffa5dc25d 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -86,18 +86,18 @@ defp parse_datetime(datetime) do parsed_datetime end - @spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) :: + @spec insert_log(%{actor: User, subject: [User], action: String.t(), permission: String.t()}) :: {:ok, ModerationLog} | {:error, any} def insert_log(%{ actor: %User{} = actor, - subject: %User{} = subject, + subject: subjects, action: action, permission: permission }) do %ModerationLog{ data: %{ "actor" => user_to_map(actor), - "subject" => user_to_map(subject), + "subject" => user_to_map(subjects), "action" => action, "permission" => permission, "message" => "" @@ -303,13 +303,16 @@ def insert_log(%{ end @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any} - defp insert_log_entry_with_message(entry) do entry.data["message"] |> put_in(get_log_entry_message(entry)) |> Repo.insert() end + defp user_to_map(users) when is_list(users) do + users |> Enum.map(&user_to_map/1) + end + defp user_to_map(%User{} = user) do user |> Map.from_struct() @@ -349,10 +352,10 @@ def get_log_entry_message(%ModerationLog{ data: %{ "actor" => %{"nickname" => actor_nickname}, "action" => "delete", - "subject" => %{"nickname" => subject_nickname, "type" => "user"} + "subject" => subjects } }) do - "@#{actor_nickname} deleted user @#{subject_nickname}" + "@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}" end @spec get_log_entry_message(ModerationLog) :: String.t() @@ -363,12 +366,7 @@ def get_log_entry_message(%ModerationLog{ "subjects" => subjects } }) do - nicknames = - subjects - |> Enum.map(&"@#{&1["nickname"]}") - |> Enum.join(", ") - - "@#{actor_nickname} created users: #{nicknames}" + "@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}" end @spec get_log_entry_message(ModerationLog) :: String.t() @@ -376,10 +374,28 @@ def get_log_entry_message(%ModerationLog{ data: %{ "actor" => %{"nickname" => actor_nickname}, "action" => "activate", - "subject" => %{"nickname" => subject_nickname, "type" => "user"} + "subject" => user + } + }) + when is_map(user) do + get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "activate", + "subject" => [user] + } + }) + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "activate", + "subject" => users } }) do - "@#{actor_nickname} activated user @#{subject_nickname}" + "@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}" end @spec get_log_entry_message(ModerationLog) :: String.t() @@ -387,10 +403,28 @@ def get_log_entry_message(%ModerationLog{ data: %{ "actor" => %{"nickname" => actor_nickname}, "action" => "deactivate", - "subject" => %{"nickname" => subject_nickname, "type" => "user"} + "subject" => user + } + }) + when is_map(user) do + get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "deactivate", + "subject" => [user] + } + }) + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "deactivate", + "subject" => users } }) do - "@#{actor_nickname} deactivated user @#{subject_nickname}" + "@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}" end @spec get_log_entry_message(ModerationLog) :: String.t() @@ -402,14 +436,9 @@ def get_log_entry_message(%ModerationLog{ "action" => "tag" } }) do - nicknames_string = - nicknames - |> Enum.map(&"@#{&1}") - |> Enum.join(", ") - tags_string = tags |> Enum.join(", ") - "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_string}" + "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}" end @spec get_log_entry_message(ModerationLog) :: String.t() @@ -421,14 +450,9 @@ def get_log_entry_message(%ModerationLog{ "action" => "untag" } }) do - nicknames_string = - nicknames - |> Enum.map(&"@#{&1}") - |> Enum.join(", ") - tags_string = tags |> Enum.join(", ") - "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_string}" + "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}" end @spec get_log_entry_message(ModerationLog) :: String.t() @@ -436,11 +460,31 @@ def get_log_entry_message(%ModerationLog{ data: %{ "actor" => %{"nickname" => actor_nickname}, "action" => "grant", - "subject" => %{"nickname" => subject_nickname}, + "subject" => user, + "permission" => permission + } + }) + when is_map(user) do + get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "grant", + "subject" => [user], + "permission" => permission + } + }) + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "grant", + "subject" => users, "permission" => permission } }) do - "@#{actor_nickname} made @#{subject_nickname} #{permission}" + "@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}" end @spec get_log_entry_message(ModerationLog) :: String.t() @@ -448,11 +492,31 @@ def get_log_entry_message(%ModerationLog{ data: %{ "actor" => %{"nickname" => actor_nickname}, "action" => "revoke", - "subject" => %{"nickname" => subject_nickname}, + "subject" => user, + "permission" => permission + } + }) + when is_map(user) do + get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "revoke", + "subject" => [user], + "permission" => permission + } + }) + end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "revoke", + "subject" => users, "permission" => permission } }) do - "@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}" + "@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}" end @spec get_log_entry_message(ModerationLog) :: String.t() @@ -551,4 +615,27 @@ def get_log_entry_message(%ModerationLog{ }) do "@#{actor_nickname} deleted status ##{subject_id}" end + + @spec get_log_entry_message(ModerationLog) :: String.t() + def get_log_entry_message(%ModerationLog{ + data: %{ + "actor" => %{"nickname" => actor_nickname}, + "action" => "force_password_reset", + "subject" => subjects + } + }) do + "@#{actor_nickname} force password reset for users: #{users_to_nicknames_string(subjects)}" + end + + defp nicknames_to_string(nicknames) do + nicknames + |> Enum.map(&"@#{&1}") + |> Enum.join(", ") + end + + defp users_to_nicknames_string(users) do + users + |> Enum.map(&"@#{&1["nickname"]}") + |> Enum.join(", ") + end end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index d94ae5971..b7ecf51e4 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -17,6 +17,7 @@ defmodule Pleroma.Notification do import Ecto.Query import Ecto.Changeset + require Logger @type t :: %__MODULE__{} @@ -34,43 +35,97 @@ def changeset(%Notification{} = notification, attrs) do end def for_user_query(user, opts \\ []) do - query = - Notification - |> where(user_id: ^user.id) + Notification + |> where(user_id: ^user.id) + |> where( + [n, a], + fragment( + "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')", + a.actor + ) + ) + |> join(:inner, [n], activity in assoc(n, :activity)) + |> join(:left, [n, a], object in Object, + on: + fragment( + "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)", + object.data, + a.data + ) + ) + |> preload([n, a, o], activity: {a, object: o}) + |> exclude_muted(user, opts) + |> exclude_blocked(user) + |> exclude_visibility(opts) + end + + defp exclude_blocked(query, user) do + query + |> where([n, a], a.actor not in ^user.blocks) + |> where( + [n, a], + fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks + ) + end + + defp exclude_muted(query, _, %{with_muted: true}) do + query + end + + defp exclude_muted(query, user, _opts) do + query + |> where([n, a], a.actor not in ^user.muted_notifications) + |> 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 + + @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 |> where( [n, a], - fragment( - "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')", - a.actor + not fragment( + "activity_visibility(?, ?, ?) = ANY (?)", + a.actor, + a.recipients, + a.data, + ^visibility ) ) - |> join(:inner, [n], activity in assoc(n, :activity)) - |> join(:left, [n, a], object in Object, - on: - fragment( - "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)", - object.data, - a.data - ) - ) - |> preload([n, a, o], activity: {a, object: o}) - - if opts[:with_muted] do - query else - where(query, [n, a], a.actor not in ^user.info.muted_notifications) - |> where([n, a], a.actor not in ^user.info.blocks) - |> where( - [n, a], - fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks - ) - |> 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)) + Logger.error("Could not exclude visibility to #{visibility}") + query end end + 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 + ) + ) + end + + defp exclude_visibility(query, %{exclude_visibilities: visibility}) + when visibility not in @valid_visibilities do + Logger.error("Could not exclude visibility to #{visibility}") + query + end + + defp exclude_visibility(query, _visibility), do: query + def for_user(user, opts \\ %{}) do user |> for_user_query(opts) @@ -259,7 +314,7 @@ def skip?(:self, activity, user) do def skip?( :followers, activity, - %{info: %{notification_settings: %{"followers" => false}}} = user + %{notification_settings: %{"followers" => false}} = user ) do actor = activity.data["actor"] follower = User.get_cached_by_ap_id(actor) @@ -269,14 +324,14 @@ def skip?( def skip?( :non_followers, activity, - %{info: %{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, %{info: %{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) @@ -285,7 +340,7 @@ def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => fa def skip?( :non_follows, activity, - %{info: %{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 cdfbacb0e..d9b41d710 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -181,7 +181,7 @@ def increase_replies_count(ap_id) do data: fragment( """ - jsonb_set(?, '{repliesCount}', + safe_jsonb_set(?, '{repliesCount}', (coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true) """, o.data, @@ -204,7 +204,7 @@ def decrease_replies_count(ap_id) do data: fragment( """ - jsonb_set(?, '{repliesCount}', + safe_jsonb_set(?, '{repliesCount}', (greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true) """, o.data, diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index f077a9f32..68535c09e 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -32,6 +32,23 @@ def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) get_actor(%{"actor" => actor}) end + # TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus + # objects being present in the test suite environment. Once these objects are + # removed, please also remove this. + if Mix.env() == :test do + defp compare_uris(_, %URI{scheme: "tag"}), do: :ok + end + + defp compare_uris(%URI{} = id_uri, %URI{} = other_uri) do + if id_uri.host == other_uri.host do + :ok + else + :error + end + end + + defp compare_uris(_, _), do: :error + @doc """ Checks that an imported AP object's actor matches the domain it came from. """ @@ -41,11 +58,7 @@ def contain_origin(id, %{"actor" => _actor} = params) do id_uri = URI.parse(id) actor_uri = URI.parse(get_actor(params)) - if id_uri.host == actor_uri.host do - :ok - else - :error - end + compare_uris(actor_uri, id_uri) end def contain_origin(id, %{"attributedTo" => actor} = params), @@ -57,11 +70,7 @@ def contain_origin_from_id(id, %{"id" => other_id} = _params) do id_uri = URI.parse(id) other_uri = URI.parse(other_id) - if id_uri.host == other_uri.host do - :ok - else - :error - end + compare_uris(id_uri, other_uri) end def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}), diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 5e064fd87..9a9a46550 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -10,7 +10,6 @@ defmodule Pleroma.Object.Fetcher do alias Pleroma.Signature alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.OStatus require Logger require Pleroma.Constants @@ -39,7 +38,8 @@ defp reinject_object(struct, data) do data <- maybe_reinject_internal_fields(data, struct), changeset <- Object.change(struct, %{data: data}), changeset <- touch_changeset(changeset), - {:ok, object} <- Repo.insert_or_update(changeset) do + {:ok, object} <- Repo.insert_or_update(changeset), + {:ok, object} <- Object.set_cache(object) do {:ok, object} else e -> @@ -54,7 +54,7 @@ def refetch_object(%Object{data: %{"id" => id}} = object) do {:ok, object} <- reinject_object(object, data) do {:ok, object} else - {:local, true} -> object + {:local, true} -> {:ok, object} e -> {:error, e} end end @@ -67,7 +67,8 @@ def fetch_object_from_id(id, options \\ []) do {:normalize, nil} <- {:normalize, Object.normalize(data, false)}, params <- prepare_activity_params(data), {:containment, :ok} <- {:containment, Containment.contain_origin(id, params)}, - {:ok, activity} <- Transmogrifier.handle_incoming(params, options), + {:transmogrifier, {:ok, activity}} <- + {:transmogrifier, Transmogrifier.handle_incoming(params, options)}, {:object, _data, %Object{} = object} <- {:object, data, Object.normalize(activity, false)} do {:ok, object} @@ -75,9 +76,12 @@ def fetch_object_from_id(id, options \\ []) do {:containment, _} -> {:error, "Object containment failed."} - {:error, {:reject, nil}} -> + {:transmogrifier, {:error, {:reject, nil}}} -> {:reject, nil} + {:transmogrifier, _} -> + {:error, "Transmogrifier failure."} + {:object, data, nil} -> reinject_object(%Object{}, data) @@ -87,15 +91,11 @@ def fetch_object_from_id(id, options \\ []) do {:fetch_object, %Object{} = object} -> {:ok, object} - _e -> - # Only fallback when receiving a fetch/normalization error with ActivityPub - Logger.info("Couldn't get object via AP, trying out OStatus fetching...") + {:fetch, {:error, error}} -> + {:error, error} - # FIXME: OStatus Object Containment? - case OStatus.fetch_activity_from_url(id) do - {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)} - e -> e - end + e -> + e end end @@ -114,7 +114,11 @@ def fetch_object_from_id!(id, options \\ []) do with {:ok, object} <- fetch_object_from_id(id, options) do object else - _e -> + {:error, %Tesla.Mock.Error{}} -> + nil + + e -> + Logger.error("Error while fetching #{id}: #{inspect(e)}") nil end end @@ -161,7 +165,7 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do Logger.debug("Fetch headers: #{inspect(headers)}") - with true <- String.starts_with?(id, "http"), + with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")}, {:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers), {:ok, data} <- Jason.decode(body), :ok <- Containment.contain_origin_from_id(id, data) do @@ -170,6 +174,12 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do {:ok, %{status: code}} when code in [404, 410] -> {:error, "Object has been deleted"} + {:scheme, _} -> + {:error, "Unsupported URI scheme"} + + {:error, e} -> + {:error, e} + e -> {:error, e} end diff --git a/lib/pleroma/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/plugs/admin_secret_authentication_plug.ex index 5baf8a691..fdadd476e 100644 --- a/lib/pleroma/plugs/admin_secret_authentication_plug.ex +++ b/lib/pleroma/plugs/admin_secret_authentication_plug.ex @@ -19,7 +19,7 @@ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call(%{params: %{"admin_token" => admin_token}} = conn, _) do if secret_token() && admin_token == secret_token() do conn - |> assign(:user, %User{info: %{is_admin: true}}) + |> assign(:user, %User{is_admin: true}) else conn end diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex index 86bc4aa3a..fd004fcd2 100644 --- a/lib/pleroma/plugs/oauth_plug.ex +++ b/lib/pleroma/plugs/oauth_plug.ex @@ -71,7 +71,7 @@ defp fetch_user_and_token(token) do ) # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength - with %Token{user: %{info: %{deactivated: false} = _} = user} = token_record <- Repo.one(query) do + with %Token{user: %{deactivated: false} = user} = token_record <- Repo.one(query) do {:ok, user, token_record} end end diff --git a/lib/pleroma/plugs/trailing_format_plug.ex b/lib/pleroma/plugs/trailing_format_plug.ex index ce366b218..a4b8a406d 100644 --- a/lib/pleroma/plugs/trailing_format_plug.ex +++ b/lib/pleroma/plugs/trailing_format_plug.ex @@ -24,7 +24,8 @@ defmodule Pleroma.Plugs.TrailingFormatPlug do "/api/help", "/api/externalprofile", "/notice", - "/api/pleroma/emoji" + "/api/pleroma/emoji", + "/api/oauth_tokens" ] def init(opts) do diff --git a/lib/pleroma/plugs/user_enabled_plug.ex b/lib/pleroma/plugs/user_enabled_plug.ex index da892c28b..fbb4bf115 100644 --- a/lib/pleroma/plugs/user_enabled_plug.ex +++ b/lib/pleroma/plugs/user_enabled_plug.ex @@ -10,7 +10,7 @@ def init(options) do options end - def call(%{assigns: %{user: %User{info: %{deactivated: true}}}} = conn, _) do + def call(%{assigns: %{user: %User{deactivated: true}}} = conn, _) do conn |> assign(:user, nil) end diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index 4c4b3d610..ee808f31f 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -11,7 +11,7 @@ def init(options) do options end - def call(%{assigns: %{user: %User{info: %{is_admin: true}}}} = conn, _) do + def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do conn end diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index 78144cae3..2ed719315 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -401,11 +401,9 @@ defp increase_read_duration(_) do defp client, do: Pleroma.ReverseProxy.Client - defp track_failed_url(url, code, opts) do - code = to_string(code) - + defp track_failed_url(url, error, opts) do ttl = - if code in ["403", "404"] or String.starts_with?(code, "5") do + unless error in [:body_too_large, 400, 204] do Keyword.get(opts, :failed_request_ttl, @failed_request_ttl) else nil diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index df80fbaa4..8154a09b7 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -68,12 +68,7 @@ defp get_stat_data do domain_count = Enum.count(peers) - status_query = - from(u in User.Query.build(%{local: true}), - select: fragment("sum((?->>'note_count')::int)", u.info) - ) - - status_count = Repo.one(status_query) + status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count) user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id) diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 9f0adde5b..2e0986197 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -105,7 +105,7 @@ defp get_opts(opts) do {Pleroma.Config.get!([:instance, :upload_limit]), "Document"} end - opts = %{ + %{ activity_type: Keyword.get(opts, :activity_type, activity_type), size_limit: Keyword.get(opts, :size_limit, size_limit), uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])), @@ -118,37 +118,6 @@ defp get_opts(opts) do Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url()) ) } - - # TODO: 1.0+ : remove old config compatibility - opts = - if Pleroma.Config.get([__MODULE__, :strip_exif]) == true && - !Enum.member?(opts.filters, Pleroma.Upload.Filter.Mogrify) do - Logger.warn(""" - Pleroma: configuration `:instance, :strip_exif` is deprecated, please instead set: - - :pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Mogrify]] - - :pleroma, Pleroma.Upload.Filter.Mogrify, args: ["strip", "auto-orient"] - """) - - Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: ["strip", "auto-orient"]) - Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Mogrify]) - else - opts - end - - if Pleroma.Config.get([:instance, :dedupe_media]) == true && - !Enum.member?(opts.filters, Pleroma.Upload.Filter.Dedupe) do - Logger.warn(""" - Pleroma: configuration `:instance, :dedupe_media` is deprecated, please instead set: - - :pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Dedupe]] - """) - - Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Dedupe]) - else - opts - end end defp prepare_upload(%Plug.Upload{} = file, opts) do diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 2cfb13a8c..f8c2db1e1 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -13,6 +13,7 @@ defmodule Pleroma.User do alias Pleroma.Activity alias Pleroma.Conversation.Participation alias Pleroma.Delivery + alias Pleroma.FollowingRelationship alias Pleroma.Keys alias Pleroma.Notification alias Pleroma.Object @@ -26,9 +27,7 @@ defmodule Pleroma.User do alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils alias Pleroma.Web.OAuth - alias Pleroma.Web.OStatus alias Pleroma.Web.RelMe - alias Pleroma.Web.Websub alias Pleroma.Workers.BackgroundWorker require Logger @@ -52,7 +51,6 @@ defmodule Pleroma.User do field(:password, :string, virtual: true) field(:password_confirmation, :string, virtual: true) field(:keys, :string) - field(:following, {:array, :string}, default: []) field(:ap_id, :string) field(:avatar, :map) field(:local, :boolean, default: true) @@ -63,15 +61,70 @@ 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: %{}) + field(:note_count, :integer, default: 0) + field(:follower_count, :integer, default: 0) + # Should be filled in only for remote users + field(:following_count, :integer, default: nil) + field(:locked, :boolean, default: false) + field(:confirmation_pending, :boolean, default: false) + field(:password_reset_pending, :boolean, default: false) + field(:confirmation_token, :string, default: nil) + field(:default_scope, :string, default: "public") + field(: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) + field(:is_moderator, :boolean, default: false) + field(:is_admin, :boolean, default: false) + field(:show_role, :boolean, default: true) + field(:settings, :map, default: nil) + field(:magic_key, :string, default: nil) + field(:uri, :string, default: nil) + field(:hide_followers_count, :boolean, default: false) + field(:hide_follows_count, :boolean, default: false) + field(:hide_followers, :boolean, default: false) + field(:hide_follows, :boolean, default: false) + field(:hide_favorites, :boolean, default: true) + field(:unread_conversation_count, :integer, default: 0) + field(:pinned_activities, {:array, :string}, default: []) + field(:email_notifications, :map, default: %{"digest" => false}) + field(:mascot, :map, default: nil) + field(:emoji, {:array, :map}, default: []) + field(:pleroma_settings_store, :map, default: %{}) + field(:fields, {:array, :map}, default: []) + field(:raw_fields, {:array, :map}, default: []) + field(:discoverable, :boolean, default: false) + field(:invisible, :boolean, default: false) + field(:skip_thread_containment, :boolean, default: false) + + field(:notification_settings, :map, + default: %{ + "followers" => true, + "follows" => true, + "non_follows" => true, + "non_followers" => true + } + ) + has_many(:notifications, Notification) has_many(:registrations, Registration) has_many(:deliveries, Delivery) - embeds_one(:info, User.Info) + + field(:info, :map, default: %{}) timestamps() end - def auth_active?(%User{info: %User.Info{confirmation_pending: true}}), + def auth_active?(%User{confirmation_pending: true}), do: !Pleroma.Config.get([:instance, :account_activation_required]) def auth_active?(%User{}), do: true @@ -86,10 +139,13 @@ def visible_for?(%User{} = user, for_user) do def visible_for?(_, _), do: false - def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true - def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true + def superuser?(%User{local: true, is_admin: true}), do: true + def superuser?(%User{local: true, is_moderator: true}), do: true def superuser?(_), do: false + def invisible?(%User{invisible: true}), do: true + def invisible?(_), do: false + def avatar_url(user, options \\ []) do case user.avatar do %{"url" => [%{"href" => href} | _]} -> href @@ -98,13 +154,13 @@ def avatar_url(user, options \\ []) do end def banner_url(user, options \\ []) do - case user.info.banner do + case user.banner do %{"url" => [%{"href" => href} | _]} -> href _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png" end end - def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url + def profile_url(%User{source_data: %{"url" => url}}), do: url def profile_url(%User{ap_id: ap_id}), do: ap_id def profile_url(_), do: nil @@ -119,15 +175,15 @@ def ap_following(%User{} = user), do: "#{ap_id(user)}/following" def user_info(%User{} = user, args \\ %{}) do following_count = - Map.get(args, :following_count, user.info.following_count || following_count(user)) + Map.get(args, :following_count, user.following_count || following_count(user)) - follower_count = Map.get(args, :follower_count, user.info.follower_count) + follower_count = Map.get(args, :follower_count, user.follower_count) %{ - note_count: user.info.note_count, - locked: user.info.locked, - confirmation_pending: user.info.confirmation_pending, - default_scope: user.info.default_scope + note_count: user.note_count, + locked: user.locked, + confirmation_pending: user.confirmation_pending, + default_scope: user.default_scope } |> Map.put(:following_count, following_count) |> Map.put(:follower_count, follower_count) @@ -157,17 +213,17 @@ def set_info_cache(user, args) do @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t() def restrict_deactivated(query) do - from(u in query, - where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info) - ) + from(u in query, where: u.deactivated != ^true) end - def following_count(%User{following: []}), do: 0 + defdelegate following_count(user), to: FollowingRelationship - def following_count(%User{} = user) do - user - |> get_friends_query() - |> Repo.aggregate(:count, :id) + defp truncate_fields_param(params) do + if Map.has_key?(params, :fields) do + Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1)) + else + params + end end defp truncate_if_exists(params, key, max_length) do @@ -188,18 +244,43 @@ def remote_user_creation(params) do |> Map.put(:info, params[:info] || %{}) |> truncate_if_exists(:name, name_limit) |> truncate_if_exists(:bio, bio_limit) + |> truncate_fields_param() changeset = %User{local: false} - |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar]) + |> cast( + params, + [ + :bio, + :name, + :ap_id, + :nickname, + :avatar, + :ap_enabled, + :source_data, + :banner, + :locked, + :magic_key, + :uri, + :hide_followers, + :hide_follows, + :hide_followers_count, + :hide_follows_count, + :follower_count, + :fields, + :following_count, + :discoverable, + :invisible + ] + ) |> validate_required([:name, :ap_id]) |> unique_constraint(:nickname) |> validate_format(:nickname, @email_regex) |> validate_length(:bio, max: bio_limit) |> validate_length(:name, max: name_limit) - |> change_info(&User.Info.remote_user_creation(&1, params[:info])) + |> validate_fields(true) - case params[:info][:source_data] do + case params[:source_data] do %{"followers" => followers, "following" => following} -> changeset |> put_change(:follower_address, followers) @@ -216,11 +297,35 @@ def update_changeset(struct, params \\ %{}) do name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) struct - |> cast(params, [:bio, :name, :avatar, :following]) + |> cast( + params, + [ + :bio, + :name, + :avatar, + :locked, + :no_rich_text, + :default_scope, + :banner, + :hide_follows, + :hide_followers, + :hide_followers_count, + :hide_follows_count, + :hide_favorites, + :background, + :show_role, + :skip_thread_containment, + :fields, + :raw_fields, + :pleroma_settings_store, + :discoverable + ] + ) |> unique_constraint(:nickname) |> validate_format(:nickname, local_nickname_regex()) |> validate_length(:bio, max: bio_limit) |> validate_length(:name, min: 1, max: name_limit) + |> validate_fields(false) end def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do @@ -229,20 +334,38 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now()) + params = if remote?, do: truncate_fields_param(params), else: params + struct - |> cast(params, [ - :bio, - :name, - :follower_address, - :following_address, - :avatar, - :last_refreshed_at - ]) + |> cast( + params, + [ + :bio, + :name, + :follower_address, + :following_address, + :avatar, + :last_refreshed_at, + :ap_enabled, + :source_data, + :banner, + :locked, + :magic_key, + :follower_count, + :following_count, + :hide_follows, + :fields, + :hide_followers, + :discoverable, + :hide_followers_count, + :hide_follows_count + ] + ) |> unique_constraint(:nickname) |> validate_format(:nickname, local_nickname_regex()) |> validate_length(:bio, max: bio_limit) |> validate_length(:name, max: name_limit) - |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?)) + |> validate_fields(remote?) end def password_update_changeset(struct, params) do @@ -250,8 +373,8 @@ def password_update_changeset(struct, params) do |> cast(params, [:password, :password_confirmation]) |> validate_required([:password, :password_confirmation]) |> validate_confirmation(:password) - |> put_password_hash - |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false)) + |> put_password_hash() + |> put_change(:password_reset_pending, false) end @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} @@ -268,19 +391,19 @@ def reset_password(%User{id: user_id} = user, data) do end end + def update_password_reset_pending(user, value) do + user + |> change() + |> put_change(:password_reset_pending, value) + |> update_and_set_cache() + end + def force_password_reset_async(user) do BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id}) end @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} - def force_password_reset(user) do - info_cng = User.Info.set_password_reset_pending(user.info, true) - - user - |> change() - |> put_embed(:info, info_cng) - |> update_and_set_cache() - end + def force_password_reset(user), do: update_password_reset_pending(user, true) def register_changeset(struct, params \\ %{}, opts \\ []) do bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) @@ -294,6 +417,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do end struct + |> confirmation_changeset(need_confirmation: need_confirmation?) |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation]) |> validate_required([:name, :nickname, :password, :password_confirmation]) |> validate_confirmation(:password) @@ -304,7 +428,6 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do |> validate_format(:email, @email_regex) |> validate_length(:bio, max: bio_limit) |> validate_length(:name, min: 1, max: name_limit) - |> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?)) |> maybe_validate_required_email(opts[:external]) |> put_password_hash |> put_ap_id() @@ -324,7 +447,6 @@ defp put_following_and_follower_address(changeset) do followers = ap_followers(%User{nickname: get_field(changeset, :nickname)}) changeset - |> put_change(:following, [followers]) |> put_change(:follower_address, followers) end @@ -355,7 +477,7 @@ def post_register_action(%User{} = user) do end def try_send_confirmation_email(%User{} = user) do - if user.info.confirmation_pending && + if user.confirmation_pending && Pleroma.Config.get([:instance, :account_activation_required]) do user |> Pleroma.Emails.UserEmail.account_confirmation_email() @@ -378,8 +500,8 @@ def needs_update?(%User{local: false} = user) do def needs_update?(_), do: true @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()} - def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do - {:ok, follower} + def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do + follow(follower, followed, "pending") end def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do @@ -397,58 +519,32 @@ def maybe_direct_follow(%User{} = follower, %User{} = followed) do @doc "A mass follow for local users. Respects blocks in both directions but does not create activities." @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()} def follow_all(follower, followeds) do - followed_addresses = - followeds - |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end) - |> Enum.map(fn %{follower_address: fa} -> fa end) + followeds = + Enum.reject(followeds, fn followed -> + blocks?(follower, followed) || blocks?(followed, follower) + end) - q = - from(u in User, - where: u.id == ^follower.id, - update: [ - set: [ - following: - fragment( - "array(select distinct unnest (array_cat(?, ?)))", - u.following, - ^followed_addresses - ) - ] - ], - select: u - ) - - {1, [follower]} = Repo.update_all(q, []) + Enum.each(followeds, &follow(follower, &1, "accept")) Enum.each(followeds, &update_follower_count/1) set_cache(follower) end - def follow(%User{} = follower, %User{info: info} = followed) do + defdelegate following(user), to: FollowingRelationship + + def follow(%User{} = follower, %User{} = followed, state \\ "accept") do deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) - ap_followers = followed.follower_address cond do - info.deactivated -> - {:error, "Could not follow user: You are deactivated."} + followed.deactivated -> + {:error, "Could not follow user: #{followed.nickname} is deactivated."} deny_follow_blocked and blocks?(followed, follower) -> {:error, "Could not follow user: #{followed.nickname} blocked you."} true -> - if !followed.local && follower.local && !ap_enabled?(followed) do - Websub.subscribe(follower, followed) - end - - q = - from(u in User, - where: u.id == ^follower.id, - update: [push: [following: ^ap_followers]], - select: u - ) - - {1, [follower]} = Repo.update_all(q, []) + FollowingRelationship.follow(follower, followed, state) follower = maybe_update_following_count(follower) @@ -459,17 +555,8 @@ def follow(%User{} = follower, %User{info: info} = followed) do end def unfollow(%User{} = follower, %User{} = followed) do - ap_followers = followed.follower_address - if following?(follower, followed) and follower.ap_id != followed.ap_id do - q = - from(u in User, - where: u.id == ^follower.id, - update: [pull: [following: ^ap_followers]], - select: u - ) - - {1, [follower]} = Repo.update_all(q, []) + FollowingRelationship.unfollow(follower, followed) follower = maybe_update_following_count(follower) @@ -483,13 +570,10 @@ def unfollow(%User{} = follower, %User{} = followed) do end end - @spec following?(User.t(), User.t()) :: boolean - def following?(%User{} = follower, %User{} = followed) do - Enum.member?(follower.following, followed.follower_address) - end + defdelegate following?(follower, followed), to: FollowingRelationship def locked?(%User{} = user) do - user.info.locked || false + user.locked || false end def get_by_id(id) do @@ -532,6 +616,12 @@ def set_cache(%User{} = user) do {:ok, user} end + def update_and_set_cache(struct, params) do + struct + |> update_changeset(params) + |> update_and_set_cache() + end + def update_and_set_cache(changeset) do with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do set_cache(user) @@ -614,12 +704,7 @@ def get_cached_user_info(user) do Cachex.fetch!(:user_cache, key, fn -> user_info(user) end) end - def fetch_by_nickname(nickname) do - case ActivityPub.make_user_from_nickname(nickname) do - {:ok, user} -> {:ok, user} - _ -> OStatus.make_user(nickname) - end - end + def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname) def get_or_fetch_by_nickname(nickname) do with %User{} = user <- get_by_nickname(nickname) do @@ -707,30 +792,12 @@ def get_friends_ids(user, page \\ nil) do |> Repo.all() end - @spec get_follow_requests(User.t()) :: {:ok, [User.t()]} - def get_follow_requests(%User{} = user) do - user - |> Activity.follow_requests_for_actor() - |> join(:inner, [a], u in User, on: a.actor == u.ap_id) - |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address])) - |> group_by([a, u], u.id) - |> select([a, u], u) - |> Repo.all() - end + defdelegate get_follow_requests(user), to: FollowingRelationship def increase_note_count(%User{} = user) do User |> where(id: ^user.id) - |> update([u], - set: [ - info: - fragment( - "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)", - u.info, - u.info - ) - ] - ) + |> update([u], inc: [note_count: 1]) |> select([u], u) |> Repo.update_all([]) |> case do @@ -744,12 +811,7 @@ def decrease_note_count(%User{} = user) do |> where(id: ^user.id) |> update([u], set: [ - info: - fragment( - "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)", - u.info, - u.info - ) + note_count: fragment("greatest(0, note_count - 1)") ] ) |> select([u], u) @@ -760,28 +822,18 @@ def decrease_note_count(%User{} = user) do end end - def update_note_count(%User{} = user) do + def update_note_count(%User{} = user, note_count \\ nil) do note_count = - from( - a in Object, - where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data), - select: count(a.id) - ) - |> Repo.one() - - update_info(user, &User.Info.set_note_count(&1, note_count)) - end - - def update_mascot(user, url) do - info_changeset = - User.Info.mascot_update( - user.info, - url - ) + note_count || + from( + a in Object, + where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data), + select: count(a.id) + ) + |> Repo.one() user - |> change() - |> put_embed(:info, info_changeset) + |> cast(%{note_count: note_count}, [:note_count]) |> update_and_set_cache() end @@ -799,10 +851,24 @@ def maybe_fetch_follow_information(user) do def fetch_follow_information(user) do with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do - update_info(user, &User.Info.follow_information_update(&1, info)) + user + |> follow_information_changeset(info) + |> update_and_set_cache() end end + defp follow_information_changeset(user, params) do + user + |> cast(params, [ + :hide_followers, + :hide_follows, + :follower_count, + :following_count, + :hide_followers_count, + :hide_follows_count + ]) + end + def update_follower_count(%User{} = user) do if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do follower_count_query = @@ -813,14 +879,7 @@ def update_follower_count(%User{} = user) do |> where(id: ^user.id) |> join(:inner, [u], s in subquery(follower_count_query)) |> update([u, s], - set: [ - info: - fragment( - "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)", - u.info, - s.count - ) - ] + set: [follower_count: s.count] ) |> select([u], u) |> Repo.update_all([]) @@ -850,14 +909,7 @@ def set_unread_conversation_count(%User{local: true} = user) do User |> join(:inner, [u], p in subquery(unread_query)) |> update([u, p], - set: [ - info: - fragment( - "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)", - u.info, - p.count - ) - ] + set: [unread_conversation_count: p.count] ) |> where([u], u.id == ^user.id) |> select([u], u) @@ -868,7 +920,7 @@ def set_unread_conversation_count(%User{local: true} = user) do end end - def set_unread_conversation_count(_), do: :noop + def set_unread_conversation_count(user), do: {:ok, user} def increment_unread_conversation_count(conversation, %User{local: true} = user) do unread_query = @@ -878,14 +930,7 @@ def increment_unread_conversation_count(conversation, %User{local: true} = user) User |> join(:inner, [u], p in subquery(unread_query)) |> update([u, p], - set: [ - info: - fragment( - "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)", - u.info, - u.info - ) - ] + inc: [unread_conversation_count: 1] ) |> where([u], u.id == ^user.id) |> where([u, p], p.count == 0) @@ -897,19 +942,7 @@ def increment_unread_conversation_count(conversation, %User{local: true} = user) end end - def increment_unread_conversation_count(_, _), do: :noop - - def remove_duplicated_following(%User{following: following} = user) do - uniq_following = Enum.uniq(following) - - if length(following) == length(uniq_following) do - {:ok, user} - else - user - |> update_changeset(%{following: uniq_following}) - |> update_and_set_cache() - end - end + def increment_unread_conversation_count(_, user), do: {:ok, user} @spec get_users_from_set([String.t()], boolean()) :: [User.t()] def get_users_from_set(ap_ids, local_only \\ true) do @@ -928,11 +961,11 @@ def get_recipients_from_activity(%Activity{recipients: to}) do @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()} def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do - update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?)) + add_to_mutes(muter, ap_id, notifications?) end def unmute(muter, %{ap_id: ap_id}) do - update_info(muter, &User.Info.remove_from_mutes(&1, ap_id)) + remove_from_mutes(muter, ap_id) end def subscribe(subscriber, %{ap_id: ap_id}) do @@ -942,14 +975,14 @@ def subscribe(subscriber, %{ap_id: ap_id}) do if blocks?(subscribed, subscriber) and deny_follow_blocked do {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"} else - update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id)) + User.add_to_subscribers(subscribed, subscriber.ap_id) end end end def unsubscribe(unsubscriber, %{ap_id: ap_id}) do with %User{} = user <- get_cached_by_ap_id(ap_id) do - update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id)) + User.remove_from_subscribers(user, unsubscriber.ap_id) end end @@ -981,8 +1014,8 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do if following?(blocked, blocker), do: unfollow(blocked, blocker) {:ok, blocker} = update_follower_count(blocker) - - update_info(blocker, &User.Info.add_to_block(&1, ap_id)) + {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked) + add_to_block(blocker, ap_id) end # helper to handle the block given only an actor's AP id @@ -991,17 +1024,17 @@ def block(blocker, %{ap_id: ap_id}) do end def unblock(blocker, %{ap_id: ap_id}) do - update_info(blocker, &User.Info.remove_from_block(&1, ap_id)) + remove_from_block(blocker, ap_id) end def mutes?(nil, _), do: false - def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id) + def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id) @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.info.muted_notifications, ap_id) + do: Enum.member?(user.muted_notifications, ap_id) def blocks?(%User{} = user, %User{} = target) do blocks_ap_id?(user, target) || blocks_domain?(user, target) @@ -1010,13 +1043,13 @@ def blocks?(%User{} = user, %User{} = target) do def blocks?(nil, _), do: false def blocks_ap_id?(%User{} = user, %User{} = target) do - Enum.member?(user.info.blocks, target.ap_id) + Enum.member?(user.blocks, target.ap_id) end def blocks_ap_id?(_, _), do: false def blocks_domain?(%User{} = user, %User{} = target) do - domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks) + domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) %{host: host} = URI.parse(target.ap_id) Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host) end @@ -1025,51 +1058,75 @@ def blocks_domain?(_, _), do: false def subscribed_to?(user, %{ap_id: ap_id}) do with %User{} = target <- get_cached_by_ap_id(ap_id) do - Enum.member?(target.info.subscribers, user.ap_id) + Enum.member?(target.subscribers, user.ap_id) end end @spec muted_users(User.t()) :: [User.t()] def muted_users(user) do - User.Query.build(%{ap_id: user.info.mutes, deactivated: false}) + User.Query.build(%{ap_id: user.mutes, deactivated: false}) |> Repo.all() end @spec blocked_users(User.t()) :: [User.t()] def blocked_users(user) do - User.Query.build(%{ap_id: user.info.blocks, deactivated: false}) + User.Query.build(%{ap_id: user.blocks, deactivated: false}) |> Repo.all() end @spec subscribers(User.t()) :: [User.t()] def subscribers(user) do - User.Query.build(%{ap_id: user.info.subscribers, deactivated: false}) + User.Query.build(%{ap_id: user.subscribers, deactivated: false}) |> Repo.all() end - def block_domain(user, domain) do - update_info(user, &User.Info.add_to_domain_block(&1, domain)) - end - - def unblock_domain(user, domain) do - update_info(user, &User.Info.remove_from_domain_block(&1, domain)) - end - def deactivate_async(user, status \\ true) do BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status}) end - def deactivate(%User{} = user, status \\ true) do - with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do + def deactivate(user, status \\ true) + + def deactivate(users, status) when is_list(users) do + Repo.transaction(fn -> + for user <- users, do: deactivate(user, status) + end) + end + + def deactivate(%User{} = user, status) do + with {:ok, user} <- set_activation_status(user, status) do Enum.each(get_followers(user), &invalidate_cache/1) - Enum.each(get_friends(user), &update_follower_count/1) + + # Only update local user counts, remote will be update during the next pull. + user + |> get_friends() + |> Enum.filter(& &1.local) + |> Enum.each(&update_follower_count/1) {:ok, user} end end - def update_notification_settings(%User{} = user, settings \\ %{}) do - update_info(user, &User.Info.update_notification_settings(&1, settings)) + 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]) + |> validate_required([:notification_settings]) + |> update_and_set_cache() + end + + def delete(users) when is_list(users) do + for user <- users, do: delete(user) end def delete(%User{} = user) do @@ -1107,7 +1164,7 @@ def perform(:fetch_initial_posts, %User{} = user) do pages = Pleroma.Config.get!([:fetch_initial_posts, :pages]) # Insert all the posts in reverse order, so they're in the right order on the timeline - user.info.source_data["outbox"] + user.source_data["outbox"] |> Utils.fetch_ordered_collection(pages) |> Enum.reverse() |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1) @@ -1228,24 +1285,13 @@ defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do defp delete_activity(_activity), do: "Doing nothing" - def html_filter_policy(%User{info: %{no_rich_text: true}}) do + def html_filter_policy(%User{no_rich_text: true}) do Pleroma.HTML.Scrubber.TwitterText end def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy]) - def fetch_by_ap_id(ap_id) do - case ActivityPub.make_user_from_ap_id(ap_id) do - {:ok, user} -> - {:ok, user} - - _ -> - case OStatus.make_user(ap_id) do - {:ok, user} -> {:ok, user} - _ -> {:error, "Could not fetch by AP id"} - end - end - end + def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id) def get_or_fetch_by_ap_id(ap_id) do user = get_cached_by_ap_id(ap_id) @@ -1275,7 +1321,7 @@ def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do else _ -> {:ok, user} = - %User{info: %User.Info{}} + %User{} |> cast(%{}, [:ap_id, :nickname, :local]) |> put_change(:ap_id, uri) |> put_change(:nickname, nickname) @@ -1288,9 +1334,7 @@ def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do end # AP style - def public_key_from_info(%{ - source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}} - }) do + def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do key = public_key_pem |> :public_key.pem_decode() @@ -1300,16 +1344,11 @@ def public_key_from_info(%{ {:ok, key} end - # OStatus Magic Key - def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do - {:ok, Pleroma.Web.Salmon.decode_key(magic_key)} - end - - def public_key_from_info(_), do: {:error, "not found key"} + def public_key(_), do: {:error, "not found key"} def get_public_key_for_ap_id(ap_id) do with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), - {:ok, public_key} <- public_key_from_info(user.info) do + {:ok, public_key} <- public_key(user) do {:ok, public_key} else _ -> :error @@ -1328,7 +1367,7 @@ def insert_or_update_user(data) do end def ap_enabled?(%User{local: true}), do: true - def ap_enabled?(%User{info: info}), do: info.ap_enabled + def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled def ap_enabled?(_), do: false @doc "Gets or fetch a user by uri or nickname." @@ -1441,7 +1480,6 @@ def error_user(ap_id) do %User{ name: ap_id, ap_id: ap_id, - info: %User.Info{}, nickname: "erroruser@example.com", inserted_at: NaiveDateTime.utc_now() } @@ -1454,7 +1492,7 @@ def all_superusers do end def showing_reblogs?(%User{} = user, %User{} = target) do - target.ap_id not in user.info.muted_reblogs + target.ap_id not in user.muted_reblogs end @doc """ @@ -1486,7 +1524,7 @@ def list_inactive_users_query(inactivity_threshold \\ 7) do left_join: a in Pleroma.Activity, on: u.ap_id == a.actor, where: not is_nil(u.nickname), - where: fragment("not (?->'deactivated' @> 'true')", u.info), + where: u.deactivated != ^true, where: u.id not in ^has_read_notifications, group_by: u.id, having: @@ -1500,16 +1538,16 @@ def list_inactive_users_query(inactivity_threshold \\ 7) do ## Examples - iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true) - Pleroma.User{info: %{email_notifications: %{"digest" => true}}} + iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true) + Pleroma.User{email_notifications: %{"digest" => true}} - iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false) - Pleroma.User{info: %{email_notifications: %{"digest" => false}}} + iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false) + Pleroma.User{email_notifications: %{"digest" => false}} """ @spec switch_email_notifications(t(), String.t(), boolean()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} def switch_email_notifications(user, type, status) do - update_info(user, &User.Info.update_email_notifications(&1, %{type => status})) + User.update_email_notifications(user, %{type => status}) end @doc """ @@ -1529,17 +1567,16 @@ def touch_last_digest_emailed_at(user) do @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()} def toggle_confirmation(%User{} = user) do - need_confirmation? = !user.info.confirmation_pending - user - |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?)) + |> confirmation_changeset(need_confirmation: !user.confirmation_pending) + |> update_and_set_cache() end - def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do + def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do mascot end - def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do + def get_mascot(%{mascot: mascot}) when is_nil(mascot) do # use instance-default config = Pleroma.Config.get([:assets, :mascots]) default_mascot = Pleroma.Config.get([:assets, :default_mascot]) @@ -1609,25 +1646,285 @@ def change_email(user, email) do |> update_and_set_cache() end - @doc """ - Changes `user.info` and returns the user changeset. - - `fun` is called with the `user.info`. - """ - def change_info(user, fun) do - changeset = change(user) - info = get_field(changeset, :info) || %User.Info{} - put_embed(changeset, :info, fun.(info)) + # Internal function; public one is `deactivate/2` + defp set_activation_status(user, deactivated) do + user + |> cast(%{deactivated: deactivated}, [:deactivated]) + |> update_and_set_cache() end - @doc """ - Updates `user.info` and sets cache. - - `fun` is called with the `user.info`. - """ - def update_info(user, fun) do + def update_banner(user, banner) do user - |> change_info(fun) + |> cast(%{banner: banner}, [:banner]) + |> update_and_set_cache() + end + + def update_background(user, background) do + user + |> cast(%{background: background}, [:background]) + |> update_and_set_cache() + end + + def update_source_data(user, source_data) do + user + |> cast(%{source_data: source_data}, [:source_data]) + |> update_and_set_cache() + end + + def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do + %{ + admin: is_admin, + moderator: is_moderator + } + end + + # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``. + # For example: [{"name": "Pronoun", "value": "she/her"}, …] + def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do + limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0) + + attachment + |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) + |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) + |> Enum.take(limit) + end + + def fields(%{fields: nil}), do: [] + + def fields(%{fields: fields}), do: fields + + def validate_fields(changeset, remote? \\ false) do + limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields + limit = Pleroma.Config.get([:instance, limit_name], 0) + + changeset + |> validate_length(:fields, max: limit) + |> validate_change(:fields, fn :fields, fields -> + if Enum.all?(fields, &valid_field?/1) do + [] + else + [fields: "invalid"] + end + end) + end + + defp valid_field?(%{"name" => name, "value" => value}) do + name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255) + value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) + + is_binary(name) && is_binary(value) && String.length(name) <= name_limit && + String.length(value) <= value_limit + end + + defp valid_field?(_), do: false + + defp truncate_field(%{"name" => name, "value" => value}) do + {name, _chopped} = + String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255)) + + {value, _chopped} = + String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255)) + + %{"name" => name, "value" => value} + end + + def admin_api_update(user, params) do + user + |> cast(params, [ + :is_moderator, + :is_admin, + :show_role + ]) + |> update_and_set_cache() + end + + def mascot_update(user, url) do + user + |> cast(%{mascot: url}, [:mascot]) + |> validate_required([:mascot]) + |> update_and_set_cache() + end + + def mastodon_settings_update(user, settings) do + user + |> cast(%{settings: settings}, [:settings]) + |> validate_required([:settings]) + |> update_and_set_cache() + end + + @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t() + def confirmation_changeset(user, need_confirmation: need_confirmation?) do + params = + if need_confirmation? do + %{ + confirmation_pending: true, + confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64() + } + else + %{ + confirmation_pending: false, + confirmation_token: nil + } + end + + cast(user, params, [:confirmation_pending, :confirmation_token]) + end + + def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do + if id not in user.pinned_activities do + max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0) + params = %{pinned_activities: user.pinned_activities ++ [id]} + + user + |> cast(params, [:pinned_activities]) + |> validate_length(:pinned_activities, + max: max_pinned_statuses, + message: "You have already pinned the maximum number of statuses" + ) + else + change(user) + end + |> update_and_set_cache() + end + + def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do + params = %{pinned_activities: List.delete(user.pinned_activities, id)} + + user + |> cast(params, [:pinned_activities]) + |> update_and_set_cache() + end + + def update_email_notifications(user, settings) do + email_notifications = + user.email_notifications + |> Map.merge(settings) + |> Map.take(["digest"]) + + params = %{email_notifications: email_notifications} + fields = [:email_notifications] + + user + |> cast(params, fields) + |> validate_required(fields) + |> update_and_set_cache() + end + + defp set_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} + + user + |> cast(params, [:domain_blocks]) + |> validate_required([:domain_blocks]) + |> update_and_set_cache() + end + + def block_domain(user, domain_blocked) do + set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks])) + end + + def unblock_domain(user, domain_blocked) do + set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked)) + end + + defp set_blocks(user, blocks) do + params = %{blocks: blocks} + + user + |> cast(params, [:blocks]) + |> validate_required([:blocks]) + |> update_and_set_cache() + end + + def add_to_block(user, blocked) do + set_blocks(user, Enum.uniq([blocked | user.blocks])) + 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? + ) + 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 + ) + 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} + + user + |> cast(params, [:invisible]) + |> validate_required([:invisible]) |> update_and_set_cache() end end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex deleted file mode 100644 index 4b5b43d7f..000000000 --- a/lib/pleroma/user/info.ex +++ /dev/null @@ -1,478 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.Info do - use Ecto.Schema - import Ecto.Changeset - - alias Pleroma.User.Info - - @type t :: %__MODULE__{} - - embedded_schema do - field(:banner, :map, default: %{}) - field(:background, :map, default: %{}) - field(:source_data, :map, default: %{}) - field(:note_count, :integer, default: 0) - field(:follower_count, :integer, default: 0) - # Should be filled in only for remote users - field(:following_count, :integer, default: nil) - field(:locked, :boolean, default: false) - field(:confirmation_pending, :boolean, default: false) - field(:password_reset_pending, :boolean, default: false) - field(:confirmation_token, :string, default: nil) - field(:default_scope, :string, default: "public") - field(: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) - field(:is_moderator, :boolean, default: false) - field(:is_admin, :boolean, default: false) - field(:show_role, :boolean, default: true) - field(:keys, :string, default: nil) - field(:settings, :map, default: nil) - field(:magic_key, :string, default: nil) - field(:uri, :string, default: nil) - field(:topic, :string, default: nil) - field(:hub, :string, default: nil) - field(:salmon, :string, default: nil) - field(:hide_followers_count, :boolean, default: false) - field(:hide_follows_count, :boolean, default: false) - field(:hide_followers, :boolean, default: false) - field(:hide_follows, :boolean, default: false) - field(:hide_favorites, :boolean, default: true) - field(:unread_conversation_count, :integer, default: 0) - field(:pinned_activities, {:array, :string}, default: []) - field(:email_notifications, :map, default: %{"digest" => false}) - field(:mascot, :map, default: nil) - field(:emoji, {:array, :map}, default: []) - field(:pleroma_settings_store, :map, default: %{}) - field(:fields, {:array, :map}, default: nil) - field(:raw_fields, {:array, :map}, default: []) - field(:discoverable, :boolean, default: false) - - field(:notification_settings, :map, - default: %{ - "followers" => true, - "follows" => true, - "non_follows" => true, - "non_followers" => true - } - ) - - field(:skip_thread_containment, :boolean, default: false) - - # Found in the wild - # ap_id -> Where is this used? - # bio -> Where is this used? - # avatar -> Where is this used? - # fqn -> Where is this used? - # host -> Where is this used? - # subject _> Where is this used? - end - - def set_activation_status(info, deactivated) do - params = %{deactivated: deactivated} - - info - |> cast(params, [:deactivated]) - |> validate_required([:deactivated]) - end - - def set_password_reset_pending(info, pending) do - params = %{password_reset_pending: pending} - - info - |> cast(params, [:password_reset_pending]) - |> validate_required([:password_reset_pending]) - end - - def update_notification_settings(info, settings) do - settings = - settings - |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end) - |> Map.new() - - notification_settings = - info.notification_settings - |> Map.merge(settings) - |> Map.take(["followers", "follows", "non_follows", "non_followers"]) - - params = %{notification_settings: notification_settings} - - info - |> cast(params, [:notification_settings]) - |> validate_required([:notification_settings]) - end - - @doc """ - Update email notifications in the given User.Info struct. - - Examples: - - iex> update_email_notifications(%Pleroma.User.Info{email_notifications: %{"digest" => false}}, %{"digest" => true}) - %Pleroma.User.Info{email_notifications: %{"digest" => true}} - - """ - @spec update_email_notifications(t(), map()) :: Ecto.Changeset.t() - def update_email_notifications(info, settings) do - email_notifications = - info.email_notifications - |> Map.merge(settings) - |> Map.take(["digest"]) - - params = %{email_notifications: email_notifications} - fields = [:email_notifications] - - info - |> cast(params, fields) - |> validate_required(fields) - end - - def add_to_note_count(info, number) do - set_note_count(info, info.note_count + number) - end - - def set_note_count(info, number) do - params = %{note_count: Enum.max([0, number])} - - info - |> cast(params, [:note_count]) - |> validate_required([:note_count]) - end - - def set_follower_count(info, number) do - params = %{follower_count: Enum.max([0, number])} - - info - |> cast(params, [:follower_count]) - |> validate_required([:follower_count]) - end - - def set_mutes(info, mutes) do - params = %{mutes: mutes} - - info - |> cast(params, [:mutes]) - |> validate_required([:mutes]) - end - - @spec set_notification_mutes(Changeset.t(), [String.t()], boolean()) :: Changeset.t() - def set_notification_mutes(changeset, muted_notifications, notifications?) do - if notifications? do - put_change(changeset, :muted_notifications, muted_notifications) - |> validate_required([:muted_notifications]) - else - changeset - end - end - - def set_blocks(info, blocks) do - params = %{blocks: blocks} - - info - |> cast(params, [:blocks]) - |> validate_required([:blocks]) - end - - def set_subscribers(info, subscribers) do - params = %{subscribers: subscribers} - - info - |> cast(params, [:subscribers]) - |> validate_required([:subscribers]) - end - - @spec add_to_mutes(Info.t(), String.t(), boolean()) :: Changeset.t() - def add_to_mutes(info, muted, notifications?) do - info - |> set_mutes(Enum.uniq([muted | info.mutes])) - |> set_notification_mutes( - Enum.uniq([muted | info.muted_notifications]), - notifications? - ) - end - - @spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t() - def remove_from_mutes(info, muted) do - info - |> set_mutes(List.delete(info.mutes, muted)) - |> set_notification_mutes(List.delete(info.muted_notifications, muted), true) - end - - def add_to_block(info, blocked) do - set_blocks(info, Enum.uniq([blocked | info.blocks])) - end - - def remove_from_block(info, blocked) do - set_blocks(info, List.delete(info.blocks, blocked)) - end - - def add_to_subscribers(info, subscribed) do - set_subscribers(info, Enum.uniq([subscribed | info.subscribers])) - end - - def remove_from_subscribers(info, subscribed) do - set_subscribers(info, List.delete(info.subscribers, subscribed)) - end - - def set_domain_blocks(info, domain_blocks) do - params = %{domain_blocks: domain_blocks} - - info - |> cast(params, [:domain_blocks]) - |> validate_required([:domain_blocks]) - end - - def add_to_domain_block(info, domain_blocked) do - set_domain_blocks(info, Enum.uniq([domain_blocked | info.domain_blocks])) - end - - def remove_from_domain_block(info, domain_blocked) do - set_domain_blocks(info, List.delete(info.domain_blocks, domain_blocked)) - end - - def set_keys(info, keys) do - params = %{keys: keys} - - info - |> cast(params, [:keys]) - |> validate_required([:keys]) - end - - def remote_user_creation(info, params) do - params = - if Map.has_key?(params, :fields) do - Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1)) - else - params - end - - info - |> cast(params, [ - :ap_enabled, - :source_data, - :banner, - :locked, - :magic_key, - :uri, - :hub, - :topic, - :salmon, - :hide_followers, - :hide_follows, - :hide_followers_count, - :hide_follows_count, - :follower_count, - :fields, - :following_count, - :discoverable - ]) - |> validate_fields(true) - end - - def user_upgrade(info, params, remote? \\ false) do - info - |> cast(params, [ - :ap_enabled, - :source_data, - :banner, - :locked, - :magic_key, - :follower_count, - :following_count, - :hide_follows, - :fields, - :hide_followers, - :discoverable, - :hide_followers_count, - :hide_follows_count - ]) - |> validate_fields(remote?) - end - - def profile_update(info, params) do - info - |> cast(params, [ - :locked, - :no_rich_text, - :default_scope, - :banner, - :hide_follows, - :hide_followers, - :hide_followers_count, - :hide_follows_count, - :hide_favorites, - :background, - :show_role, - :skip_thread_containment, - :fields, - :raw_fields, - :pleroma_settings_store, - :discoverable - ]) - |> validate_fields() - end - - def validate_fields(changeset, remote? \\ false) do - limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields - limit = Pleroma.Config.get([:instance, limit_name], 0) - - changeset - |> validate_length(:fields, max: limit) - |> validate_change(:fields, fn :fields, fields -> - if Enum.all?(fields, &valid_field?/1) do - [] - else - [fields: "invalid"] - end - end) - end - - defp valid_field?(%{"name" => name, "value" => value}) do - name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255) - value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) - - is_binary(name) && is_binary(value) && String.length(name) <= name_limit && - String.length(value) <= value_limit - end - - defp valid_field?(_), do: false - - defp truncate_field(%{"name" => name, "value" => value}) do - {name, _chopped} = - String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255)) - - {value, _chopped} = - String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255)) - - %{"name" => name, "value" => value} - end - - @spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t() - def confirmation_changeset(info, opts) do - need_confirmation? = Keyword.get(opts, :need_confirmation) - - params = - if need_confirmation? do - %{ - confirmation_pending: true, - confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64() - } - else - %{ - confirmation_pending: false, - confirmation_token: nil - } - end - - cast(info, params, [:confirmation_pending, :confirmation_token]) - end - - def mastodon_settings_update(info, settings) do - params = %{settings: settings} - - info - |> cast(params, [:settings]) - |> validate_required([:settings]) - end - - def mascot_update(info, url) do - params = %{mascot: url} - - info - |> cast(params, [:mascot]) - |> validate_required([:mascot]) - end - - def set_source_data(info, source_data) do - params = %{source_data: source_data} - - info - |> cast(params, [:source_data]) - |> validate_required([:source_data]) - end - - def admin_api_update(info, params) do - info - |> cast(params, [ - :is_moderator, - :is_admin, - :show_role - ]) - end - - def add_pinnned_activity(info, %Pleroma.Activity{id: id}) do - if id not in info.pinned_activities do - max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0) - params = %{pinned_activities: info.pinned_activities ++ [id]} - - info - |> cast(params, [:pinned_activities]) - |> validate_length(:pinned_activities, - max: max_pinned_statuses, - message: "You have already pinned the maximum number of statuses" - ) - else - change(info) - end - end - - def remove_pinnned_activity(info, %Pleroma.Activity{id: id}) do - params = %{pinned_activities: List.delete(info.pinned_activities, id)} - - cast(info, params, [:pinned_activities]) - end - - def roles(%Info{is_moderator: is_moderator, is_admin: is_admin}) do - %{ - admin: is_admin, - moderator: is_moderator - } - end - - def add_reblog_mute(info, ap_id) do - params = %{muted_reblogs: info.muted_reblogs ++ [ap_id]} - - cast(info, params, [:muted_reblogs]) - end - - def remove_reblog_mute(info, ap_id) do - params = %{muted_reblogs: List.delete(info.muted_reblogs, ap_id)} - - cast(info, params, [:muted_reblogs]) - end - - # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``. - # For example: [{"name": "Pronoun", "value": "she/her"}, …] - def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do - limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0) - - attachment - |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) - |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) - |> Enum.take(limit) - end - - def fields(%{fields: nil}), do: [] - - def fields(%{fields: fields}), do: fields - - def follow_information_update(info, params) do - info - |> cast(params, [ - :hide_followers, - :hide_follows, - :follower_count, - :following_count, - :hide_followers_count, - :hide_follows_count - ]) - end -end diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 2baf016cf..364bc1c89 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -28,6 +28,8 @@ defmodule Pleroma.User.Query do """ import Ecto.Query import Pleroma.Web.AdminAPI.Search, only: [not_empty_string: 1] + + alias Pleroma.FollowingRelationship alias Pleroma.User @type criteria :: @@ -56,7 +58,6 @@ defmodule Pleroma.User.Query do @ilike_criteria [:nickname, :name, :query] @equal_criteria [:email] - @role_criteria [:is_admin, :is_moderator] @contains_criteria [:ap_id, :nickname] @spec build(criteria()) :: Query.t() @@ -100,15 +101,19 @@ defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 Enum.reduce(tags, query, &prepare_tag_criteria/2) end - defp compose_query({key, _}, query) when key in @role_criteria do - where(query, [u], fragment("(?->? @> 'true')", u.info, ^to_string(key))) + defp compose_query({:is_admin, _}, query) do + where(query, [u], u.is_admin) + end + + defp compose_query({:is_moderator, _}, query) do + where(query, [u], u.is_moderator) end defp compose_query({:super_users, _}, query) do where( query, [u], - fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info) + u.is_admin or u.is_moderator ) end @@ -117,7 +122,13 @@ defp compose_query({:local, _}, query), do: location_query(query, true) defp compose_query({:external, _}, query), do: location_query(query, false) defp compose_query({:active, _}, query) do - where(query, [u], fragment("not (?->'deactivated' @> 'true')", u.info)) + User.restrict_deactivated(query) + |> where([u], not is_nil(u.nickname)) + end + + defp compose_query({:legacy_active, _}, query) do + query + |> where([u], fragment("not (?->'deactivated' @> 'true')", u.info)) |> where([u], not is_nil(u.nickname)) end @@ -126,22 +137,45 @@ defp compose_query({:deactivated, false}, query) do end defp compose_query({:deactivated, true}, query) do - where(query, [u], fragment("?->'deactivated' @> 'true'", u.info)) + where(query, [u], u.deactivated == ^true) |> where([u], not is_nil(u.nickname)) end - defp compose_query({:followers, %User{id: id, follower_address: follower_address}}, query) do - where(query, [u], fragment("? <@ ?", ^[follower_address], u.following)) + defp compose_query({:followers, %User{id: id}}, query) do + query |> where([u], u.id != ^id) + |> join(:inner, [u], r in FollowingRelationship, + as: :relationships, + on: r.following_id == ^id and r.follower_id == u.id + ) + |> where([relationships: r], r.state == "accept") end - defp compose_query({:friends, %User{id: id, following: following}}, query) do - where(query, [u], u.follower_address in ^following) + defp compose_query({:friends, %User{id: id}}, query) do + query |> where([u], u.id != ^id) + |> join(:inner, [u], r in FollowingRelationship, + as: :relationships, + on: r.following_id == u.id and r.follower_id == ^id + ) + |> where([relationships: r], r.state == "accept") end defp compose_query({:recipients_from_activity, to}, query) do - where(query, [u], u.ap_id in ^to or fragment("? && ?", u.following, ^to)) + query + |> join(:left, [u], r in FollowingRelationship, + as: :relationships, + on: r.follower_id == u.id + ) + |> join(:left, [relationships: r], f in User, + as: :following, + on: f.id == r.following_id + ) + |> where( + [u, following: f, relationships: r], + u.ap_id in ^to or (f.follower_address in ^to and r.state == "accept") + ) + |> distinct(true) end defp compose_query({:order_by, key}, query) do diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 6fb2c2352..09664db76 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -4,11 +4,9 @@ defmodule Pleroma.User.Search do alias Pleroma.Pagination - alias Pleroma.Repo alias Pleroma.User import Ecto.Query - @similarity_threshold 0.25 @limit 20 def search(query_string, opts \\ []) do @@ -23,18 +21,10 @@ def search(query_string, opts \\ []) do maybe_resolve(resolve, for_user, query_string) - {:ok, results} = - Repo.transaction(fn -> - Ecto.Adapters.SQL.query( - Repo, - "select set_limit(#{@similarity_threshold})", - [] - ) - - query_string - |> search_query(for_user, following) - |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset) - end) + results = + query_string + |> search_query(for_user, following) + |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset) results end @@ -56,26 +46,66 @@ defp search_query(query_string, for_user, following) do |> base_query(following) |> filter_blocked_user(for_user) |> filter_blocked_domains(for_user) - |> search_subqueries(query_string) - |> union_subqueries - |> distinct_query() - |> boost_search_rank_query(for_user) + |> fts_search(query_string) + |> trigram_rank(query_string) + |> boost_search_rank(for_user) |> subquery() |> order_by(desc: :search_rank) |> maybe_restrict_local(for_user) end + defp fts_search(query, query_string) do + query_string = to_tsquery(query_string) + + from( + u in query, + where: + fragment( + """ + (to_tsvector('simple', ?) || to_tsvector('simple', ?)) @@ to_tsquery('simple', ?) + """, + u.name, + u.nickname, + ^query_string + ) + ) + end + + defp to_tsquery(query_string) do + String.trim_trailing(query_string, "@" <> local_domain()) + |> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ") + |> String.trim() + |> String.split() + |> Enum.map(&(&1 <> ":*")) + |> Enum.join(" | ") + end + + defp trigram_rank(query, query_string) do + from( + u in query, + select_merge: %{ + search_rank: + fragment( + "similarity(?, trim(? || ' ' || coalesce(?, '')))", + ^query_string, + u.nickname, + u.name + ) + } + ) + end + defp base_query(_user, false), do: User defp base_query(user, true), do: User.get_followers_query(user) - defp filter_blocked_user(query, %User{info: %{blocks: blocks}}) + defp filter_blocked_user(query, %User{blocks: blocks}) when length(blocks) > 0 do from(q in query, where: not (q.ap_id in ^blocks)) end defp filter_blocked_user(query, _), do: query - defp filter_blocked_domains(query, %User{info: %{domain_blocks: domain_blocks}}) + defp filter_blocked_domains(query, %User{domain_blocks: domain_blocks}) when length(domain_blocks) > 0 do domains = Enum.join(domain_blocks, ",") @@ -87,21 +117,6 @@ defp filter_blocked_domains(query, %User{info: %{domain_blocks: domain_blocks}}) defp filter_blocked_domains(query, _), do: query - defp union_subqueries({fts_subquery, trigram_subquery}) do - from(s in trigram_subquery, union_all: ^fts_subquery) - end - - defp search_subqueries(base_query, query_string) do - { - fts_search_subquery(base_query, query_string), - trigram_search_subquery(base_query, query_string) - } - end - - defp distinct_query(q) do - from(s in subquery(q), order_by: s.search_type, distinct: s.id) - end - defp maybe_resolve(true, user, query) do case {limit(), user} do {:all, _} -> :noop @@ -126,9 +141,9 @@ defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauth defp restrict_local(q), do: where(q, [u], u.local == true) - defp boost_search_rank_query(query, nil), do: query + defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) - defp boost_search_rank_query(query, for_user) do + defp boost_search_rank(query, %User{} = for_user) do friends_ids = User.get_friends_ids(for_user) followers_ids = User.get_followers_ids(for_user) @@ -137,8 +152,8 @@ defp boost_search_rank_query(query, for_user) do search_rank: fragment( """ - CASE WHEN (?) THEN 0.5 + (?) * 1.3 - WHEN (?) THEN 0.5 + (?) * 1.2 + CASE WHEN (?) THEN (?) * 1.5 + WHEN (?) THEN (?) * 1.3 WHEN (?) THEN (?) * 1.1 ELSE (?) END """, @@ -154,70 +169,5 @@ defp boost_search_rank_query(query, for_user) do ) end - @spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t() - defp fts_search_subquery(query, term) do - processed_query = - String.trim_trailing(term, "@" <> local_domain()) - |> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ") - |> String.trim() - |> String.split() - |> Enum.map(&(&1 <> ":*")) - |> Enum.join(" | ") - - from( - u in query, - select_merge: %{ - search_type: ^0, - search_rank: - fragment( - """ - ts_rank_cd( - setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') || - setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'), - to_tsquery('simple', ?), - 32 - ) - """, - u.nickname, - u.name, - ^processed_query - ) - }, - where: - fragment( - """ - (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') || - setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?) - """, - u.nickname, - u.name, - ^processed_query - ) - ) - |> User.restrict_deactivated() - end - - @spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t() - defp trigram_search_subquery(query, term) do - term = String.trim_trailing(term, "@" <> local_domain()) - - from( - u in query, - select_merge: %{ - # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason - search_type: fragment("?", 1), - search_rank: - fragment( - "similarity(?, trim(? || ' ' || coalesce(?, '')))", - ^term, - u.nickname, - u.name - ) - }, - where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term) - ) - |> User.restrict_deactivated() - end - - defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) + defp boost_search_rank(query, _for_user), do: query end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index f92d5b398..3b75e9d85 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Activity.Ir.Topics alias Pleroma.Config alias Pleroma.Conversation + alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Containment @@ -68,7 +69,7 @@ defp get_recipients(data) do defp check_actor_is_active(actor) do if not is_nil(actor) do with user <- User.get_cached_by_ap_id(actor), - false <- user.info.deactivated do + false <- user.deactivated do true else _e -> false @@ -131,7 +132,7 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when {:ok, map} <- MRF.filter(map), {recipients, _, _} = get_recipients(map), {:fake, false, map, recipients} <- {:fake, fake, map, recipients}, - :ok <- Containment.contain_child(map), + {:containment, :ok} <- {:containment, Containment.contain_child(map)}, {:ok, map, object} <- insert_full_object(map) do {:ok, activity} = Repo.insert(%Activity{ @@ -153,11 +154,8 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when Notification.create_notifications(activity) - participations = - activity - |> Conversation.create_or_bump_for() - |> get_participations() - + conversation = create_or_bump_conversation(activity, map["actor"]) + participations = get_participations(conversation) stream_out(activity) stream_out_participations(participations) {:ok, activity} @@ -182,7 +180,20 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when end end - defp get_participations({:ok, %{participations: participations}}), do: participations + defp create_or_bump_conversation(activity, actor) do + with {:ok, conversation} <- Conversation.create_or_bump_for(activity), + %User{} = user <- User.get_cached_by_ap_id(actor), + Participation.mark_as_read(user, conversation) do + {:ok, conversation} + end + end + + defp get_participations({:ok, conversation}) do + conversation + |> Repo.preload(:participations, force: true) + |> Map.get(:participations) + end + defp get_participations(_), do: [] def stream_out_participations(participations) do @@ -225,6 +236,7 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f # only accept false as false value local = !(params[:local] == false) published = params[:published] + quick_insert? = Pleroma.Config.get([:env]) == :benchmark with create_data <- make_create_data( @@ -235,12 +247,14 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f {:fake, false, activity} <- {:fake, fake, activity}, _ <- increase_replies_count_if_reply(create_data), _ <- increase_poll_votes_if_vote(create_data), - # Changing note count prior to enqueuing federation task in order to avoid - # race conditions on updating user.info + {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity}, {:ok, _actor} <- increase_note_count_if_public(actor, activity), :ok <- maybe_federate(activity) do {:ok, activity} else + {:quick_insert, true, activity} -> + {:ok, activity} + {:fake, true, activity} -> {:ok, activity} @@ -269,22 +283,21 @@ def listen(%{to: to, actor: actor, context: context, object: object} = params) d end end - def accept(%{to: to, actor: actor, object: object} = params) do - # only accept false as false value - local = !(params[:local] == false) - - with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object}, - {:ok, activity} <- insert(data, local), - :ok <- maybe_federate(activity) do - {:ok, activity} - end + def accept(params) do + accept_or_reject("Accept", params) end - def reject(%{to: to, actor: actor, object: object} = params) do - # only accept false as false value - local = !(params[:local] == false) + def reject(params) do + accept_or_reject("Reject", params) + end - with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object}, + def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do + local = Map.get(params, :local, true) + activity_id = Map.get(params, :activity_id, nil) + + with data <- + %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object} + |> Utils.maybe_put("id", activity_id), {:ok, activity} <- insert(data, local), :ok <- maybe_federate(activity) do {:ok, activity} @@ -435,23 +448,27 @@ def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do end end - def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do + def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options \\ []) do + local = Keyword.get(options, :local, true) + activity_id = Keyword.get(options, :activity_id, nil) + actor = Keyword.get(options, :actor, actor) + user = User.get_cached_by_ap_id(actor) to = (object.data["to"] || []) ++ (object.data["cc"] || []) with {:ok, object, activity} <- Object.delete(object), - data <- %{ - "type" => "Delete", - "actor" => actor, - "object" => id, - "to" => to, - "deleted_activity_id" => activity && activity.id - }, + data <- + %{ + "type" => "Delete", + "actor" => actor, + "object" => id, + "to" => to, + "deleted_activity_id" => activity && activity.id + } + |> maybe_put("id", activity_id), {:ok, activity} <- insert(data, local, false), stream_out_participations(object, user), _ <- decrease_replies_count_if_reply(object), - # Changing note count prior to enqueuing federation task in order to avoid - # race conditions on updating user.info {:ok, _actor} <- decrease_note_count_if_public(user, object), :ok <- maybe_federate(activity) do {:ok, activity} @@ -512,7 +529,8 @@ def flag( with flag_data <- make_flag_data(params, additional), {:ok, activity} <- insert(flag_data, local), - :ok <- maybe_federate(activity) do + {:ok, stripped_activity} <- strip_report_status_data(activity), + :ok <- maybe_federate(stripped_activity) do Enum.each(User.all_superusers(), fn superuser -> superuser |> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content) @@ -527,7 +545,9 @@ defp fetch_activities_for_context_query(context, opts) do public = [Pleroma.Constants.as_public()] recipients = - if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public + if opts["user"], + do: [opts["user"].ap_id | User.following(opts["user"])] ++ public, + else: public from(activity in Activity) |> maybe_preload_objects(opts) @@ -617,12 +637,55 @@ defp restrict_visibility(_query, %{visibility: visibility}) defp restrict_visibility(query, _visibility), do: query + defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + when is_list(visibility) do + if Enum.all?(visibility, &(&1 in @valid_visibilities)) do + from( + a in query, + where: + not fragment( + "activity_visibility(?, ?, ?) = ANY (?)", + a.actor, + a.recipients, + a.data, + ^visibility + ) + ) + else + Logger.error("Could not exclude visibility to #{visibility}") + query + end + end + + defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + when visibility in @valid_visibilities do + from( + a in query, + where: + not fragment( + "activity_visibility(?, ?, ?) = ?", + a.actor, + a.recipients, + a.data, + ^visibility + ) + ) + end + + defp exclude_visibility(query, %{"exclude_visibilities" => visibility}) + when visibility not in @valid_visibilities do + Logger.error("Could not exclude visibility to #{visibility}") + query + end + + defp exclude_visibility(query, _visibility), do: query + defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _), do: query defp restrict_thread_visibility( query, - %{"user" => %User{info: %{skip_thread_containment: true}}}, + %{"user" => %User{skip_thread_containment: true}}, _ ), do: query @@ -660,7 +723,7 @@ def fetch_user_activities(user, reading_user, params \\ %{}) do |> Map.put("user", reading_user) |> Map.put("actor_id", user.ap_id) |> Map.put("whole_db", true) - |> Map.put("pinned_activity_ids", user.info.pinned_activities) + |> Map.put("pinned_activity_ids", user.pinned_activities) recipients = user_activities_recipients(%{ @@ -678,7 +741,7 @@ defp user_activities_recipients(%{"godmode" => true}) do defp user_activities_recipients(%{"reading_user" => reading_user}) do if reading_user do - [Pleroma.Constants.as_public()] ++ [reading_user.ap_id | reading_user.following] + [Pleroma.Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)] else [Pleroma.Constants.as_public()] end @@ -821,8 +884,8 @@ 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{info: info}} = opts) do - mutes = info.mutes + defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do + mutes = user.mutes query = from([activity] in query, @@ -839,9 +902,9 @@ defp restrict_muted(query, %{"muting_user" => %User{info: info}} = opts) do defp restrict_muted(query, _), do: query - defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do - blocks = info.blocks || [] - domain_blocks = info.domain_blocks || [] + defp restrict_blocked(query, %{"blocking_user" => %User{} = user}) do + blocks = user.blocks || [] + domain_blocks = user.domain_blocks || [] query = if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query) @@ -882,8 +945,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{info: info}}) do - muted_reblogs = info.muted_reblogs || [] + defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user}) do + muted_reblogs = user.muted_reblogs || [] from( activity in query, @@ -981,6 +1044,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_muted_reblogs(opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) + |> exclude_visibility(opts) end def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do @@ -1067,17 +1131,17 @@ defp object_to_user_data(data) do locked = data["manuallyApprovesFollowers"] || false data = Transmogrifier.maybe_fix_user_object(data) discoverable = data["discoverable"] || false + invisible = data["invisible"] || false user_data = %{ ap_id: data["id"], - info: %{ - ap_enabled: true, - source_data: data, - banner: banner, - fields: fields, - locked: locked, - discoverable: discoverable - }, + ap_enabled: true, + source_data: data, + banner: banner, + fields: fields, + locked: locked, + discoverable: discoverable, + invisible: invisible, avatar: avatar, name: data["name"], follower_address: data["followers"], @@ -1129,7 +1193,7 @@ defp maybe_update_follow_information(data) do with {:enabled, true} <- {:enabled, Pleroma.Config.get([:instance, :external_user_synchronization])}, {:ok, info} <- fetch_follow_information_for_user(data) do - info = Map.merge(data.info, info) + info = Map.merge(data[:info] || %{}, info) Map.put(data, :info, info) else {:enabled, false} -> @@ -1180,7 +1244,9 @@ def fetch_and_prepare_user_from_ap_id(ap_id) do data <- maybe_update_follow_information(data) do {:ok, data} else - e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}") + e -> + Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}") + {:error, e} end end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 080030eb5..b2cd965fe 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -137,7 +137,7 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p with %User{} = user <- User.get_cached_by_nickname(nickname), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), {:show_follows, true} <- - {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do + {:show_follows, (for_user && for_user == user) || !user.hide_follows} do {page, _} = Integer.parse(page) conn @@ -174,7 +174,7 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p with %User{} = user <- User.get_cached_by_nickname(nickname), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), {:show_followers, true} <- - {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do + {:show_followers, (for_user && for_user == user) || !user.hide_followers} do {page, _} = Integer.parse(page) conn @@ -319,12 +319,12 @@ def read_inbox( when page? in [true, "true"] do activities = if params["max_id"] do - ActivityPub.fetch_activities([user.ap_id | user.following], %{ + ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{ "max_id" => params["max_id"], "limit" => 10 }) else - ActivityPub.fetch_activities([user.ap_id | user.following], %{"limit" => 10}) + ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10}) end conn @@ -387,7 +387,7 @@ def handle_user_activity(user, %{"type" => "Create"} = params) do def handle_user_activity(user, %{"type" => "Delete"} = params) do with %Object{} = object <- Object.normalize(params["object"]), - true <- user.info.is_moderator || user.ap_id == object.data["actor"], + true <- user.is_moderator || user.ap_id == object.data["actor"], {:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} else diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex index b90193ca0..8abe18e29 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex @@ -11,7 +11,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do # has the user successfully posted before? defp old_user?(%User{} = u) do - u.info.note_count > 0 || u.info.follower_count > 0 + u.note_count > 0 || u.follower_count > 0 end # does the post contain links? diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 3866dacee..4ea37fc7b 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -129,7 +129,7 @@ defp recipients(actor, activity) do [] end - Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers ++ fetchers + Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers end defp get_cc_ap_ids(ap_id, recipients) do @@ -140,7 +140,7 @@ defp get_cc_ap_ids(ap_id, recipients) do |> Enum.map(& &1.ap_id) end - defp maybe_use_sharedinbox(%User{info: %{source_data: data}}), + defp maybe_use_sharedinbox(%User{source_data: data}), do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] @doc """ @@ -156,7 +156,7 @@ defp maybe_use_sharedinbox(%User{info: %{source_data: data}}), """ def determine_inbox( %Activity{data: activity_data}, - %User{info: %{source_data: data}} = user + %User{source_data: data} = user ) do to = activity_data["to"] || [] cc = activity_data["cc"] || [] @@ -190,12 +190,12 @@ def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity) recipients |> Enum.filter(&User.ap_enabled?/1) - |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end) + |> Enum.map(fn %{source_data: data} -> data["inbox"] end) |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) |> Instances.filter_reachable() |> Enum.each(fn {inbox, unreachable_since} -> %User{ap_id: ap_id} = - Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end) + Enum.find(recipients, fn %{source_data: data} -> data["inbox"] == inbox end) # Get all the recipients on the same host and add them to cc. Otherwise, a remote # instance would only accept a first message for the first recipient and ignore the rest. diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index c2ac38907..fc2619680 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -10,8 +10,16 @@ defmodule Pleroma.Web.ActivityPub.Relay do require Logger def get_actor do + actor = + relay_ap_id() + |> User.get_or_create_service_actor_by_ap_id() + + {:ok, actor} = User.set_invisible(actor, true) + actor + end + + def relay_ap_id do "#{Pleroma.Web.Endpoint.url()}/relay" - |> User.get_or_create_service_actor_by_ap_id() end @spec follow(String.t()) :: {:ok, Activity.t()} | {:error, any()} @@ -51,6 +59,21 @@ def publish(%Activity{data: %{"type" => "Create"}} = activity) do def publish(_), do: {:error, "Not implemented"} + @spec list() :: {:ok, [String.t()]} | {:error, any()} + def list do + with %User{} = user <- get_actor() do + list = + user + |> User.following() + |> Enum.map(fn entry -> URI.parse(entry).host end) + |> Enum.uniq() + + {:ok, list} + else + error -> format_error(error) + end + end + defp format_error({:error, error}), do: format_error(error) defp format_error(error) do diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 2c1f01c18..15612545b 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do A module to handle coding from internal to wire ActivityPub and back. """ alias Pleroma.Activity + alias Pleroma.FollowingRelationship alias Pleroma.Object alias Pleroma.Object.Containment alias Pleroma.Repo @@ -474,7 +475,8 @@ def handle_incoming( {_, false} <- {:user_locked, User.locked?(followed)}, {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, {_, {:ok, _}} <- - {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do + {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")}, + {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do ActivityPub.accept(%{ to: [follower.ap_id], actor: followed, @@ -484,6 +486,7 @@ def handle_incoming( else {:user_blocked, true} -> {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") + {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") ActivityPub.reject(%{ to: [follower.ap_id], @@ -494,6 +497,7 @@ def handle_incoming( {:follow, {:error, _}} -> {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") + {:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") ActivityPub.reject(%{ to: [follower.ap_id], @@ -503,6 +507,7 @@ def handle_incoming( }) {:user_locked, true} -> + {:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending") :noop end @@ -514,7 +519,7 @@ def handle_incoming( end def handle_incoming( - %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data, + %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data, _options ) do with actor <- Containment.get_actor(data), @@ -522,13 +527,14 @@ def handle_incoming( {:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), - {:ok, _follower} = User.follow(follower, followed) do + {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do ActivityPub.accept(%{ to: follow_activity.data["to"], type: "Accept", actor: followed, object: follow_activity.data["id"], - local: false + local: false, + activity_id: id }) else _e -> :error @@ -536,7 +542,7 @@ def handle_incoming( end def handle_incoming( - %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data, + %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data, _options ) do with actor <- Containment.get_actor(data), @@ -544,16 +550,16 @@ def handle_incoming( {:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), + {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), {:ok, activity} <- ActivityPub.reject(%{ to: follow_activity.data["to"], type: "Reject", actor: followed, object: follow_activity.data["id"], - local: false + local: false, + activity_id: id }) do - User.unfollow(follower, followed) - {:ok, activity} else _e -> :error @@ -643,13 +649,18 @@ def handle_incoming( data, _options ) - when object_type in ["Person", "Application", "Service", "Organization"] do + when object_type in [ + "Person", + "Application", + "Service", + "Organization" + ] do with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) - banner = new_user_data[:info][:banner] - locked = new_user_data[:info][:locked] || false - attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || [] + locked = new_user_data[:locked] || false + attachment = get_in(new_user_data, [:source_data, "attachment"]) || [] + invisible = new_user_data[:invisible] || false fields = attachment @@ -658,8 +669,10 @@ def handle_incoming( update_data = new_user_data - |> Map.take([:name, :bio, :avatar]) - |> Map.put(:info, %{banner: banner, locked: locked, fields: fields}) + |> Map.take([:avatar, :banner, :bio, :name]) + |> Map.put(:fields, fields) + |> Map.put(:locked, locked) + |> Map.put(:invisible, invisible) actor |> User.upgrade_changeset(update_data, true) @@ -686,7 +699,7 @@ def handle_incoming( # an error or a tombstone. This would allow us to verify that a deletion actually took # place. def handle_incoming( - %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data, + %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data, _options ) do object_id = Utils.get_ap_id(object_id) @@ -695,7 +708,8 @@ def handle_incoming( {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), {:ok, object} <- get_obj_helper(object_id), :ok <- Containment.contain_origin(actor.ap_id, object.data), - {:ok, activity} <- ActivityPub.delete(object, false) do + {:ok, activity} <- + ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do {:ok, activity} else nil -> @@ -1047,7 +1061,7 @@ defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"} end - def take_emoji_tags(%User{info: %{emoji: emoji} = _user_info} = _user) do + def take_emoji_tags(%User{emoji: emoji}) do emoji |> Enum.flat_map(&Map.to_list/1) |> Enum.map(&build_emoji_tag/1) @@ -1122,45 +1136,22 @@ def perform(:user_upgrade, user) do # we pass a fake user so that the followers collection is stripped away old_follower_address = User.ap_followers(%User{nickname: user.nickname}) - q = - from( - u in User, - where: ^old_follower_address in u.following, - update: [ - set: [ - following: - fragment( - "array_replace(?,?,?)", - u.following, - ^old_follower_address, - ^user.follower_address - ) - ] + from( + a in Activity, + where: ^old_follower_address in a.recipients, + update: [ + set: [ + recipients: + fragment( + "array_replace(?,?,?)", + a.recipients, + ^old_follower_address, + ^user.follower_address + ) ] - ) - - Repo.update_all(q, []) - - maybe_retire_websub(user.ap_id) - - q = - from( - a in Activity, - where: ^old_follower_address in a.recipients, - update: [ - set: [ - recipients: - fragment( - "array_replace(?,?,?)", - a.recipients, - ^old_follower_address, - ^user.follower_address - ) - ] - ] - ) - - Repo.update_all(q, []) + ] + ) + |> Repo.update_all([]) end def upgrade_user_from_ap_id(ap_id) do @@ -1185,19 +1176,6 @@ defp upgrade_user(user, data) do |> User.update_and_set_cache() end - def maybe_retire_websub(ap_id) do - # some sanity checks - if is_binary(ap_id) && String.length(ap_id) > 8 do - q = - from( - ws in Pleroma.Web.Websub.WebsubClientSubscription, - where: fragment("? like ?", ws.topic, ^"#{ap_id}%") - ) - - Repo.delete_all(q) - end - end - def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do Map.put(data, "url", url["href"]) end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 6263f206b..91cfa2194 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -12,6 +12,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do alias Pleroma.User alias Pleroma.Web alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.Endpoint alias Pleroma.Web.Router.Helpers @@ -21,6 +22,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do require Pleroma.Constants @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer", "Audio"] + @strip_status_report_states ~w(closed resolved) @supported_report_states ~w(open closed resolved) @valid_visibilities ~w(public unlisted private direct) @@ -49,26 +51,28 @@ def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do def determine_explicit_mentions(_), do: [] - @spec recipient_in_collection(any(), any()) :: boolean() - defp recipient_in_collection(ap_id, coll) when is_binary(coll), do: ap_id == coll - defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll - defp recipient_in_collection(_, _), do: false + @spec label_in_collection?(any(), any()) :: boolean() + defp label_in_collection?(ap_id, coll) when is_binary(coll), do: ap_id == coll + defp label_in_collection?(ap_id, coll) when is_list(coll), do: ap_id in coll + defp label_in_collection?(_, _), do: false + + @spec label_in_message?(String.t(), map()) :: boolean() + def label_in_message?(label, params), + do: + [params["to"], params["cc"], params["bto"], params["bcc"]] + |> Enum.any?(&label_in_collection?(label, &1)) + + @spec unaddressed_message?(map()) :: boolean() + def unaddressed_message?(params), + do: + [params["to"], params["cc"], params["bto"], params["bcc"]] + |> Enum.all?(&is_nil(&1)) @spec recipient_in_message(User.t(), User.t(), map()) :: boolean() - def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params) do - addresses = [params["to"], params["cc"], params["bto"], params["bcc"]] - - cond do - Enum.any?(addresses, &recipient_in_collection(ap_id, &1)) -> true - # if the message is unaddressed at all, then assume it is directly addressed - # to the recipient - Enum.all?(addresses, &is_nil(&1)) -> true - # if the message is sent from somebody the user is following, then assume it - # is addressed to the recipient - User.following?(recipient, actor) -> true - true -> false - end - end + def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params), + do: + label_in_message?(ap_id, params) || unaddressed_message?(params) || + User.following?(recipient, actor) defp extract_list(target) when is_binary(target), do: [target] defp extract_list(lst) when is_list(lst), do: lst @@ -76,8 +80,8 @@ defp extract_list(_), do: [] def maybe_splice_recipient(ap_id, params) do need_splice? = - !recipient_in_collection(ap_id, params["to"]) && - !recipient_in_collection(ap_id, params["cc"]) + !label_in_collection?(ap_id, params["to"]) && + !label_in_collection?(ap_id, params["cc"]) if need_splice? do cc_list = extract_list(params["cc"]) @@ -582,10 +586,14 @@ def add_announce_to_object( %Activity{data: %{"actor" => actor}}, object ) do - announcements = take_announcements(object) + unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do + announcements = take_announcements(object) - with announcements <- Enum.uniq([actor | announcements]) do - update_element_in_object("announcement", announcements, object) + with announcements <- Enum.uniq([actor | announcements]) do + update_element_in_object("announcement", announcements, object) + end + else + {:ok, object} end end @@ -699,10 +707,24 @@ def make_flag_data(_, _), do: %{} defp build_flag_object(%{account: account, statuses: statuses} = _) do [account.ap_id] ++ - Enum.map(statuses || [], fn - %Activity{} = act -> act.data["id"] - act when is_map(act) -> act["id"] - act when is_binary(act) -> act + Enum.map(statuses || [], fn act -> + id = + case act do + %Activity{} = act -> act.data["id"] + act when is_map(act) -> act["id"] + 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"]) + + %{ + "type" => "Note", + "id" => activity.data["id"], + "content" => activity.object.data["content"], + "published" => activity.object.data["published"], + "actor" => AccountView.render("show.json", %{user: actor}) + } end) end @@ -749,6 +771,20 @@ def fetch_ordered_collection(from, pages_left, acc \\ []) do #### Report-related helpers + def update_report_state(%Activity{} = activity, state) + when state in @strip_status_report_states do + {:ok, stripped_activity} = strip_report_status_data(activity) + + new_data = + activity.data + |> Map.put("state", state) + |> Map.put("object", stripped_activity.data["object"]) + + activity + |> Changeset.change(data: new_data) + |> Repo.update() + end + def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do new_data = Map.put(activity.data, "state", state) @@ -759,6 +795,14 @@ def update_report_state(%Activity{} = activity, state) when state in @supported_ def update_report_state(_, _), do: {:error, "Unsupported state"} + def strip_report_status_data(activity) do + [actor | reported_activities] = activity.data["object"] + stripped_activities = Enum.map(reported_activities, & &1["id"]) + new_data = put_in(activity.data, ["object"], [actor | stripped_activities]) + + {:ok, %{activity | data: new_data}} + end + def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do [to, cc, recipients] = activity diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 9b39d1629..cf08045c9 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -55,7 +55,8 @@ def render("service.json", %{user: user}) do "owner" => user.ap_id, "publicKeyPem" => public_key }, - "endpoints" => endpoints + "endpoints" => endpoints, + "invisible" => User.invisible?(user) } |> Map.merge(Utils.make_json_ld_header()) end @@ -78,8 +79,8 @@ def render("user.json", %{user: user}) do emoji_tags = Transmogrifier.take_emoji_tags(user) fields = - user.info - |> User.Info.fields() + user + |> User.fields() |> Enum.map(fn %{"name" => name, "value" => value} -> %{ "name" => Pleroma.HTML.strip_tags(name), @@ -99,7 +100,7 @@ def render("user.json", %{user: user}) do "name" => user.name, "summary" => user.bio, "url" => user.ap_id, - "manuallyApprovesFollowers" => user.info.locked, + "manuallyApprovesFollowers" => user.locked, "publicKey" => %{ "id" => "#{user.ap_id}#main-key", "owner" => user.ap_id, @@ -107,8 +108,8 @@ def render("user.json", %{user: user}) do }, "endpoints" => endpoints, "attachment" => fields, - "tag" => (user.info.source_data["tag"] || []) ++ emoji_tags, - "discoverable" => user.info.discoverable + "tag" => (user.source_data["tag"] || []) ++ emoji_tags, + "discoverable" => user.discoverable } |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) @@ -116,8 +117,8 @@ def render("user.json", %{user: user}) do end def render("following.json", %{user: user, page: page} = opts) do - showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_follows - showing_count = showing_items || !user.info.hide_follows_count + showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows + showing_count = showing_items || !user.hide_follows_count query = User.get_friends_query(user) query = from(user in query, select: [:ap_id]) @@ -135,8 +136,8 @@ def render("following.json", %{user: user, page: page} = opts) do end def render("following.json", %{user: user} = opts) do - showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_follows - showing_count = showing_items || !user.info.hide_follows_count + showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows + showing_count = showing_items || !user.hide_follows_count query = User.get_friends_query(user) query = from(user in query, select: [:ap_id]) @@ -155,7 +156,7 @@ def render("following.json", %{user: user} = opts) do "totalItems" => total, "first" => if showing_items do - collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows) + collection(following, "#{user.ap_id}/following", 1, !user.hide_follows) else "#{user.ap_id}/following?page=1" end @@ -164,8 +165,8 @@ def render("following.json", %{user: user} = opts) do end def render("followers.json", %{user: user, page: page} = opts) do - showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_followers - showing_count = showing_items || !user.info.hide_followers_count + showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers + showing_count = showing_items || !user.hide_followers_count query = User.get_followers_query(user) query = from(user in query, select: [:ap_id]) @@ -183,8 +184,8 @@ def render("followers.json", %{user: user, page: page} = opts) do end def render("followers.json", %{user: user} = opts) do - showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_followers - showing_count = showing_items || !user.info.hide_followers_count + showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers + showing_count = showing_items || !user.hide_followers_count query = User.get_followers_query(user) query = from(user in query, select: [:ap_id]) diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 270d0fa02..cd4097493 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.ActivityPub.Utils require Pleroma.Constants @@ -15,7 +16,7 @@ def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false def is_public?(%Object{data: data}), do: is_public?(data) def is_public?(%Activity{data: data}), do: is_public?(data) def is_public?(%{"directMessage" => true}), do: false - def is_public?(data), do: Pleroma.Constants.as_public() in (data["to"] ++ (data["cc"] || [])) + def is_public?(data), do: Utils.label_in_message?(Pleroma.Constants.as_public(), data) def is_private?(activity) do with false <- is_public?(activity), @@ -58,7 +59,7 @@ def visible_for_user?(activity, nil) do end def visible_for_user?(activity, user) do - x = [user.ap_id | user.following] + x = [user.ap_id | User.following(user)] y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || []) visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y)) end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 513bae800..30fc01755 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -46,11 +46,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do :user_delete, :users_create, :user_toggle_activation, + :user_activate, + :user_deactivate, :tag_users, :untag_users, :right_add, - :right_delete, - :set_activation_status + :right_delete ] ) @@ -98,7 +99,7 @@ def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do ModerationLog.insert_log(%{ actor: admin, - subject: user, + subject: [user], action: "delete" }) @@ -106,6 +107,20 @@ def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do |> json(nickname) end + def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do + users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) + User.delete(users) + + ModerationLog.insert_log(%{ + actor: admin, + subject: users, + action: "delete" + }) + + conn + |> json(nicknames) + end + def user_follow(%{assigns: %{user: admin}} = conn, %{ "follower" => follower_nick, "followed" => followed_nick @@ -234,13 +249,13 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do user = User.get_cached_by_nickname(nickname) - {:ok, updated_user} = User.deactivate(user, !user.info.deactivated) + {:ok, updated_user} = User.deactivate(user, !user.deactivated) - action = if user.info.deactivated, do: "activate", else: "deactivate" + action = if user.deactivated, do: "activate", else: "deactivate" ModerationLog.insert_log(%{ actor: admin, - subject: user, + subject: [user], action: action }) @@ -249,6 +264,36 @@ def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => ni |> render("show.json", %{user: updated_user}) end + def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do + users = Enum.map(nicknames, &User.get_cached_by_nickname/1) + {:ok, updated_users} = User.deactivate(users, false) + + ModerationLog.insert_log(%{ + actor: admin, + subject: users, + action: "activate" + }) + + conn + |> put_view(AccountView) + |> render("index.json", %{users: Keyword.values(updated_users)}) + end + + def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do + users = Enum.map(nicknames, &User.get_cached_by_nickname/1) + {:ok, updated_users} = User.deactivate(users, true) + + ModerationLog.insert_log(%{ + actor: admin, + subject: users, + action: "deactivate" + }) + + conn + |> put_view(AccountView) + |> render("index.json", %{users: Keyword.values(updated_users)}) + end + def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do with {:ok, _} <- User.tag(nicknames, tags) do ModerationLog.insert_log(%{ @@ -289,6 +334,7 @@ def list_users(conn, params) do } with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)), + {:ok, users, count} <- filter_relay_user(users, count), do: conn |> json( @@ -300,6 +346,17 @@ def list_users(conn, params) do ) end + defp filter_relay_user(users, count) do + filtered_users = Enum.reject(users, &relay_user?/1) + count = if Enum.any?(users, &relay_user?/1), do: length(filtered_users), else: count + + {:ok, filtered_users, count} + end + + defp relay_user?(user) do + user.ap_id == Relay.relay_ap_id() + end + @filters ~w(local external active deactivated is_admin is_moderator) @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{} @@ -313,26 +370,51 @@ defp maybe_parse_filters(filters) do |> Enum.into(%{}, &{&1, true}) end + def right_add_multiple(%{assigns: %{user: admin}} = conn, %{ + "permission_group" => permission_group, + "nicknames" => nicknames + }) + when permission_group in ["moderator", "admin"] do + update = %{:"is_#{permission_group}" => true} + + users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) + + for u <- users, do: User.admin_api_update(u, update) + + ModerationLog.insert_log(%{ + action: "grant", + actor: admin, + subject: users, + permission: permission_group + }) + + json(conn, update) + end + + def right_add_multiple(conn, _) do + render_error(conn, :not_found, "No such permission_group") + end + def right_add(%{assigns: %{user: admin}} = conn, %{ "permission_group" => permission_group, "nickname" => nickname }) when permission_group in ["moderator", "admin"] do - info = Map.put(%{}, "is_" <> permission_group, true) + fields = %{:"is_#{permission_group}" => true} {:ok, user} = nickname |> User.get_cached_by_nickname() - |> User.update_info(&User.Info.admin_api_update(&1, info)) + |> User.admin_api_update(fields) ModerationLog.insert_log(%{ action: "grant", actor: admin, - subject: user, + subject: [user], permission: permission_group }) - json(conn, info) + json(conn, fields) end def right_add(conn, _) do @@ -344,13 +426,41 @@ def right_get(conn, %{"nickname" => nickname}) do conn |> json(%{ - is_moderator: user.info.is_moderator, - is_admin: user.info.is_admin + is_moderator: user.is_moderator, + is_admin: user.is_admin }) end - def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do - render_error(conn, :forbidden, "You can't revoke your own admin status.") + def right_delete_multiple( + %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn, + %{ + "permission_group" => permission_group, + "nicknames" => nicknames + } + ) + when permission_group in ["moderator", "admin"] do + with false <- Enum.member?(nicknames, admin_nickname) do + update = %{:"is_#{permission_group}" => false} + + users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) + + for u <- users, do: User.admin_api_update(u, update) + + ModerationLog.insert_log(%{ + action: "revoke", + actor: admin, + subject: users, + permission: permission_group + }) + + json(conn, update) + else + _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.") + end + end + + def right_delete_multiple(conn, _) do + render_error(conn, :not_found, "No such permission_group") end def right_delete( @@ -361,43 +471,34 @@ def right_delete( } ) when permission_group in ["moderator", "admin"] do - info = Map.put(%{}, "is_" <> permission_group, false) + fields = %{:"is_#{permission_group}" => false} {:ok, user} = nickname |> User.get_cached_by_nickname() - |> User.update_info(&User.Info.admin_api_update(&1, info)) + |> User.admin_api_update(fields) ModerationLog.insert_log(%{ action: "revoke", actor: admin, - subject: user, + subject: [user], permission: permission_group }) - json(conn, info) + json(conn, fields) end - def right_delete(conn, _) do - render_error(conn, :not_found, "No such permission_group") + def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do + render_error(conn, :forbidden, "You can't revoke your own admin status.") end - def set_activation_status(%{assigns: %{user: admin}} = conn, %{ - "nickname" => nickname, - "status" => status - }) do - with {:ok, status} <- Ecto.Type.cast(:boolean, status), - %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, _} <- User.deactivate(user, !status) do - action = if(user.info.deactivated, do: "activate", else: "deactivate") - - ModerationLog.insert_log(%{ - actor: admin, - subject: user, - action: action - }) - - json_response(conn, :no_content, "") + def relay_list(conn, _params) do + with {:ok, list} <- Relay.list() do + json(conn, %{relays: list}) + else + _ -> + conn + |> put_status(500) end end @@ -506,10 +607,16 @@ def get_password_reset(conn, %{"nickname" => nickname}) do end @doc "Force password reset for a given user" - def force_password_reset(conn, %{"nickname" => nickname}) do - (%User{local: true} = user) = User.get_cached_by_nickname(nickname) + def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do + users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) - User.force_password_reset_async(user) + Enum.map(users, &User.force_password_reset_async/1) + + ModerationLog.insert_log(%{ + actor: admin, + subject: users, + action: "force_password_reset" + }) json_response(conn, :no_content, "") end diff --git a/lib/pleroma/web/admin_api/report.ex b/lib/pleroma/web/admin_api/report.ex index c751dc2be..9c3468570 100644 --- a/lib/pleroma/web/admin_api/report.ex +++ b/lib/pleroma/web/admin_api/report.ex @@ -13,8 +13,9 @@ def extract_report_info( account = User.get_cached_by_ap_id(account_ap_id) statuses = - Enum.map(status_ap_ids, fn ap_id -> - Activity.get_by_ap_id_with_object(ap_id) + Enum.map(status_ap_ids, fn + act when is_map(act) -> Activity.get_by_ap_id_with_object(act["id"]) + act when is_binary(act) -> Activity.get_by_ap_id_with_object(act) end) %{report: report, user: user, account: account, statuses: statuses} diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index a96affd40..6aa7257ce 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Web.AdminAPI.AccountView do alias Pleroma.HTML alias Pleroma.User - alias Pleroma.User.Info alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.MediaProxy @@ -19,6 +18,12 @@ def render("index.json", %{users: users, count: count, page_size: page_size}) do } end + def render("index.json", %{users: users}) do + %{ + users: render_many(users, AccountView, "show.json", as: :user) + } + end + def render("show.json", %{user: user}) do avatar = User.avatar_url(user) |> MediaProxy.url() display_name = HTML.strip_tags(user.name || user.nickname) @@ -28,9 +33,9 @@ def render("show.json", %{user: user}) do "avatar" => avatar, "nickname" => user.nickname, "display_name" => display_name, - "deactivated" => user.info.deactivated, + "deactivated" => user.deactivated, "local" => user.local, - "roles" => Info.roles(user.info), + "roles" => User.roles(user), "tags" => user.tags || [] } end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index caf9371de..f1937b1ec 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity alias Pleroma.ActivityExpiration alias Pleroma.Conversation.Participation + alias Pleroma.FollowingRelationship alias Pleroma.Object alias Pleroma.ThreadMute alias Pleroma.User @@ -40,6 +41,7 @@ def accept_follow_request(follower, followed) do with {:ok, follower} <- User.follow(follower, followed), %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), + {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"), {:ok, _activity} <- ActivityPub.accept(%{ to: [follower.ap_id], @@ -54,6 +56,7 @@ def accept_follow_request(follower, followed) do def reject_follow_request(follower, followed) do with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), + {:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), {:ok, _activity} <- ActivityPub.reject(%{ to: [follower.ap_id], @@ -282,10 +285,10 @@ defp maybe_create_activity_expiration(result, _), do: result # Updates the emojis for a user based on their profile def update(user) do emoji = emoji_from_profile(user) - source_data = user.info |> Map.get(:source_data, %{}) |> Map.put("tag", emoji) + source_data = Map.put(user.source_data, "tag", emoji) user = - case User.update_info(user, &User.Info.set_source_data(&1, source_data)) do + case User.update_source_data(user, source_data) do {:ok, user} -> user _ -> user end @@ -306,20 +309,20 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do object: %Object{data: %{"type" => "Note"}} } = activity <- get_by_id_or_ap_id(id_or_ap_id), true <- Visibility.is_public?(activity), - {:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do + {:ok, _user} <- User.add_pinnned_activity(user, activity) do {:ok, activity} else - {:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err} + {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} _ -> {:error, dgettext("errors", "Could not pin")} end end def unpin(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), - {:ok, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do + {:ok, _user} <- User.remove_pinnned_activity(user, activity) do {:ok, activity} else - %{errors: [pinned_activities: {err, _}]} -> {:error, err} + {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err} _ -> {:error, dgettext("errors", "Could not unpin")} end end @@ -411,14 +414,14 @@ 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.info.muted_reblogs do - User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id)) + if ap_id not in user.muted_reblogs do + User.add_reblog_mute(user, ap_id) end end def show_reblogs(user, %{ap_id: ap_id} = _muted) do - if ap_id in user.info.muted_reblogs do - User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id)) + if ap_id in user.muted_reblogs do + User.remove_reblog_mute(user, ap_id) end end end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 2212e93f4..49735b5c2 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.Endpoint do plug(Pleroma.Plugs.HTTPSecurityPlug) plug(Pleroma.Plugs.UploadedMedia) - @static_cache_control "public, no-cache" + @static_cache_control "public max-age=86400 must-revalidate" # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files # If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index 1a2da014a..e8a56ebd7 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -10,19 +10,11 @@ defmodule Pleroma.Web.Federator do alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Federator.Publisher - alias Pleroma.Web.OStatus - alias Pleroma.Web.Websub alias Pleroma.Workers.PublisherWorker alias Pleroma.Workers.ReceiverWorker - alias Pleroma.Workers.SubscriberWorker require Logger - def init do - # To do: consider removing this call in favor of scheduled execution (`quantum`-based) - refresh_subscriptions(schedule_in: 60) - end - @doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)" # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength def allowed_incoming_reply_depth?(depth) do @@ -37,10 +29,6 @@ def allowed_incoming_reply_depth?(depth) do # Client API - def incoming_doc(doc) do - ReceiverWorker.enqueue("incoming_doc", %{"body" => doc}) - end - def incoming_ap_doc(params) do ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params}) end @@ -53,18 +41,6 @@ def publish(activity) do PublisherWorker.enqueue("publish", %{"activity_id" => activity.id}) end - def verify_websub(websub) do - SubscriberWorker.enqueue("verify_websub", %{"websub_id" => websub.id}) - end - - def request_subscription(websub) do - SubscriberWorker.enqueue("request_subscription", %{"websub_id" => websub.id}) - end - - def refresh_subscriptions(worker_args \\ []) do - SubscriberWorker.enqueue("refresh_subscriptions", %{}, worker_args ++ [max_attempts: 1]) - end - # Job Worker Callbacks @spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()} @@ -81,11 +57,6 @@ def perform(:publish, activity) do end end - def perform(:incoming_doc, doc) do - Logger.info("Got document, trying to parse") - OStatus.handle_incoming(doc) - end - def perform(:incoming_ap_doc, params) do Logger.info("Handling incoming AP activity") @@ -111,29 +82,6 @@ def perform(:incoming_ap_doc, params) do end end - def perform(:request_subscription, websub) do - Logger.debug("Refreshing #{websub.topic}") - - with {:ok, websub} <- Websub.request_subscription(websub) do - Logger.debug("Successfully refreshed #{websub.topic}") - else - _e -> Logger.debug("Couldn't refresh #{websub.topic}") - end - end - - def perform(:verify_websub, websub) do - Logger.debug(fn -> - "Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})" - end) - - Websub.verify(websub) - end - - def perform(:refresh_subscriptions) do - Logger.debug("Federator running refresh subscriptions") - Websub.refresh_subscriptions() - end - def ap_enabled_actor(id) do user = User.get_cached_by_ap_id(id) diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex index 937064638..fb9b26649 100644 --- a/lib/pleroma/web/federator/publisher.ex +++ b/lib/pleroma/web/federator/publisher.ex @@ -80,4 +80,30 @@ def gather_nodeinfo_protocol_names do links ++ module.gather_nodeinfo_protocol_names() end) end + + @doc """ + Gathers a set of remote users given an IR envelope. + """ + def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do + cc = Map.get(data, "cc", []) + + bcc = + data + |> Map.get("bcc", []) + |> Enum.reduce([], fn ap_id, bcc -> + case Pleroma.List.get_by_ap_id(ap_id) do + %Pleroma.List{user_id: ^user_id} = list -> + {:ok, following} = Pleroma.List.get_following(list) + bcc ++ Enum.map(following, & &1.ap_id) + + _ -> + bcc + end + end) + + [to, cc, bcc] + |> Enum.concat() + |> Enum.map(&User.get_cached_by_ap_id/1) + |> Enum.filter(fn user -> user && !user.local end) + end end diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index 87860f1d5..ca261ad6e 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -34,9 +34,15 @@ def index(%{assigns: %{user: user}} = conn, _params) do end end + @doc "GET /web/manifest.json" + def manifest(conn, _params) do + conn + |> render("manifest.json") + end + @doc "PUT /api/web/settings" def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do - with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do + with {:ok, _} <- User.mastodon_settings_update(user, settings) do json(conn, %{}) else e -> diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 9ef7fd48d..73fad519e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -130,25 +130,6 @@ def verify_credentials(%{assigns: %{user: user}} = conn, _) do def update_credentials(%{assigns: %{user: original_user}} = conn, params) do user = original_user - user_params = - %{} - |> add_if_present(params, "display_name", :name) - |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end) - |> add_if_present(params, "avatar", :avatar, fn value -> - with %Plug.Upload{} <- value, - {:ok, object} <- ActivityPub.upload(value, type: :avatar) do - {:ok, object.data} - end - end) - - emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") - - user_info_emojis = - user.info - |> Map.get(:emoji, []) - |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text)) - |> Enum.dedup() - params = if Map.has_key?(params, "fields_attributes") do Map.update!(params, "fields_attributes", fn fields -> @@ -160,7 +141,7 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do params end - info_params = + user_params = [ :no_rich_text, :locked, @@ -176,15 +157,13 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do |> Enum.reduce(%{}, fn key, acc -> add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)}) end) - |> add_if_present(params, "default_scope", :default_scope) - |> add_if_present(params, "fields_attributes", :fields, fn fields -> - fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) - - {:ok, fields} - end) - |> add_if_present(params, "fields_attributes", :raw_fields) - |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> - {:ok, Map.merge(user.info.pleroma_settings_store, value)} + |> add_if_present(params, "display_name", :name) + |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end) + |> add_if_present(params, "avatar", :avatar, fn value -> + with %Plug.Upload{} <- value, + {:ok, object} <- ActivityPub.upload(value, type: :avatar) do + {:ok, object.data} + end end) |> add_if_present(params, "header", :banner, fn value -> with %Plug.Upload{} <- value, @@ -198,12 +177,27 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do {:ok, object.data} end end) - |> Map.put(:emoji, user_info_emojis) + |> add_if_present(params, "fields_attributes", :fields, fn fields -> + fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) - changeset = + {:ok, fields} + end) + |> add_if_present(params, "fields_attributes", :raw_fields) + |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> + {:ok, Map.merge(user.pleroma_settings_store, value)} + end) + |> add_if_present(params, "default_scope", :default_scope) + + emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") + + user_emojis = user - |> User.update_changeset(user_params) - |> User.change_info(&User.Info.profile_update(&1, info_params)) + |> Map.get(:emoji, []) + |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text)) + |> Enum.dedup() + + user_params = Map.put(user_params, :emoji, user_emojis) + changeset = User.update_changeset(user, user_params) with {:ok, user} <- User.update_and_set_cache(changeset) do if original_user != user, do: CommonAPI.update(user) @@ -269,7 +263,7 @@ def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do followers = cond do for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params) - user.info.hide_followers -> [] + user.hide_followers -> [] true -> MastodonAPI.get_followers(user, params) end @@ -283,7 +277,7 @@ def following(%{assigns: %{user: for_user, account: user}} = conn, params) do followers = cond do for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params) - user.info.hide_follows -> [] + user.hide_follows -> [] true -> MastodonAPI.get_friends(user, params) end diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex index c7606246b..456fe7ab2 100644 --- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex @@ -21,8 +21,8 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) @doc "GET /api/v1/domain_blocks" - def index(%{assigns: %{user: %{info: info}}} = conn, _) do - json(conn, Map.get(info, :domain_blocks, [])) + def index(%{assigns: %{user: user}} = conn, _) do + json(conn, Map.get(user, :domain_blocks, [])) end @doc "POST /api/v1/domain_blocks" diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex new file mode 100644 index 000000000..ce025624d --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerController do + use Pleroma.Web, :controller + alias Pleroma.Plugs.OAuthScopesPlug + + plug( + OAuthScopesPlug, + %{scopes: ["read:statuses"]} + when action == :index + ) + + plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert) + plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + + # GET /api/v1/markers + def index(%{assigns: %{user: user}} = conn, params) do + markers = Pleroma.Marker.get_markers(user, params["timeline"]) + render(conn, "markers.json", %{markers: markers}) + end + + # POST /api/v1/markers + def upsert(%{assigns: %{user: user}} = conn, params) do + with {:ok, result} <- Pleroma.Marker.upsert(user, params), + markers <- Map.values(result) do + render(conn, "markers.json", %{markers: markers}) + end + end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 9f086a8c2..f2d2d3ccb 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do alias Pleroma.Pagination alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct]) @@ -28,7 +29,7 @@ def home(%{assigns: %{user: user}} = conn, params) do |> Map.put("muting_user", user) |> Map.put("user", user) - recipients = [user.ap_id | user.following] + recipients = [user.ap_id | User.following(user)] activities = recipients @@ -128,9 +129,12 @@ def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do # we must filter the following list for the user to avoid leaking statuses the user # does not actually have permission to see (for more info, peruse security issue #270). + + user_following = User.following(user) + activities = following - |> Enum.filter(fn x -> x in user.following end) + |> Enum.filter(fn x -> x in user_following end) |> ActivityPub.fetch_activities_bounded(following, params) |> Enum.reverse() diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index ac01d1ff3..d875a5788 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -71,6 +71,7 @@ def get_scheduled_activities(user, params \\ %{}) do defp cast_params(params) do param_types = %{ exclude_types: {:array, :string}, + exclude_visibilities: {:array, :string}, reblogs: :boolean, with_muted: :boolean } diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 2d4976891..e30fed610 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -74,23 +74,23 @@ defp do_render("show.json", %{user: user} = opts) do user_info = User.get_cached_user_info(user) following_count = - if !user.info.hide_follows_count or !user.info.hide_follows or opts[:for] == user do + if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do user_info.following_count else 0 end followers_count = - if !user.info.hide_followers_count or !user.info.hide_followers or opts[:for] == user do + if !user.hide_followers_count or !user.hide_followers or opts[:for] == user do user_info.follower_count else 0 end - bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"] + bot = (user.source_data["type"] || "Person") in ["Application", "Service"] emojis = - (user.info.source_data["tag"] || []) + (user.source_data["tag"] || []) |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> %{ @@ -102,8 +102,8 @@ defp do_render("show.json", %{user: user} = opts) do end) fields = - user.info - |> User.Info.fields() + user + |> User.fields() |> Enum.map(fn %{"name" => name, "value" => value} -> %{ "name" => Pleroma.HTML.strip_tags(name), @@ -111,23 +111,19 @@ defp do_render("show.json", %{user: user} = opts) do } end) - raw_fields = Map.get(user.info, :raw_fields, []) - bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for])) relationship = render("relationship.json", %{user: opts[:for], target: user}) - discoverable = user.info.discoverable - %{ id: to_string(user.id), username: username_from_nickname(user.nickname), acct: user.nickname, display_name: display_name, - locked: user_info.locked, + locked: user.locked, created_at: Utils.to_masto_date(user.inserted_at), followers_count: followers_count, following_count: following_count, - statuses_count: user_info.note_count, + statuses_count: user.note_count, note: bio || "", url: User.profile_url(user), avatar: image, @@ -140,9 +136,9 @@ defp do_render("show.json", %{user: user} = opts) do source: %{ note: HTML.strip_tags((user.bio || "") |> String.replace("
", "\n")), sensitive: false, - fields: raw_fields, + fields: user.raw_fields, pleroma: %{ - discoverable: discoverable + discoverable: user.discoverable } }, @@ -150,14 +146,14 @@ defp do_render("show.json", %{user: user} = opts) do pleroma: %{ confirmation_pending: user_info.confirmation_pending, tags: user.tags, - hide_followers_count: user.info.hide_followers_count, - hide_follows_count: user.info.hide_follows_count, - hide_followers: user.info.hide_followers, - hide_follows: user.info.hide_follows, - hide_favorites: user.info.hide_favorites, + hide_followers_count: user.hide_followers_count, + hide_follows_count: user.hide_follows_count, + hide_followers: user.hide_followers, + hide_follows: user.hide_follows, + hide_favorites: user.hide_favorites, relationship: relationship, - skip_thread_containment: user.info.skip_thread_containment, - background_image: image_url(user.info.background) |> MediaProxy.url() + skip_thread_containment: user.skip_thread_containment, + background_image: image_url(user.background) |> MediaProxy.url() } } |> maybe_put_role(user, opts[:for]) @@ -195,21 +191,21 @@ defp maybe_put_settings( data, %User{id: user_id} = user, %User{id: user_id}, - user_info + _user_info ) do data - |> Kernel.put_in([:source, :privacy], user_info.default_scope) - |> Kernel.put_in([:source, :pleroma, :show_role], user.info.show_role) - |> Kernel.put_in([:source, :pleroma, :no_rich_text], user.info.no_rich_text) + |> Kernel.put_in([:source, :privacy], user.default_scope) + |> Kernel.put_in([:source, :pleroma, :show_role], user.show_role) + |> Kernel.put_in([:source, :pleroma, :no_rich_text], user.no_rich_text) end defp maybe_put_settings(data, _, _, _), do: data - defp maybe_put_settings_store(data, %User{info: info, id: id}, %User{id: id}, %{ + defp maybe_put_settings_store(data, %User{} = user, %User{}, %{ with_pleroma_settings: true }) do data - |> Kernel.put_in([:pleroma, :settings_store], info.pleroma_settings_store) + |> Kernel.put_in([:pleroma, :settings_store], user.pleroma_settings_store) end defp maybe_put_settings_store(data, _, _, _), do: data @@ -223,28 +219,28 @@ defp maybe_put_chat_token(data, %User{id: id}, %User{id: id}, %{ defp maybe_put_chat_token(data, _, _, _), do: data - defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do + defp maybe_put_role(data, %User{show_role: true} = user, _) do data - |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) - |> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator) + |> Kernel.put_in([:pleroma, :is_admin], user.is_admin) + |> Kernel.put_in([:pleroma, :is_moderator], user.is_moderator) end defp maybe_put_role(data, %User{id: user_id} = user, %User{id: user_id}) do data - |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) - |> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator) + |> Kernel.put_in([:pleroma, :is_admin], user.is_admin) + |> Kernel.put_in([:pleroma, :is_moderator], user.is_moderator) end defp maybe_put_role(data, _, _), do: data defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do - Kernel.put_in(data, [:pleroma, :notification_settings], user.info.notification_settings) + Kernel.put_in(data, [:pleroma, :notification_settings], user.notification_settings) end defp maybe_put_notification_settings(data, _, _), do: data - defp maybe_put_activation_status(data, user, %User{info: %{is_admin: true}}) do - Kernel.put_in(data, [:pleroma, :deactivated], user.info.deactivated) + defp maybe_put_activation_status(data, user, %User{is_admin: true}) do + Kernel.put_in(data, [:pleroma, :deactivated], user.deactivated) end defp maybe_put_activation_status(data, _, _), do: data @@ -253,7 +249,7 @@ defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{ data |> Kernel.put_in( [:pleroma, :unread_conversation_count], - user.info.unread_conversation_count + user.unread_conversation_count ) end diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index e9d2735b3..c5998e661 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -34,7 +34,11 @@ def render("participation.json", %{participation: participation, for: user}) do id: participation.id |> to_string(), accounts: render(AccountView, "index.json", users: users, as: :user), unread: !participation.read, - last_status: render(StatusView, "show.json", activity: activity, for: user) + last_status: + render(StatusView, "show.json", + activity: activity, + direct_conversation_id: participation.id + ) } end end diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex new file mode 100644 index 000000000..38fbeed5f --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerView do + use Pleroma.Web, :view + + def render("markers.json", %{markers: markers}) do + Enum.reduce(markers, %{}, fn m, acc -> + Map.put_new(acc, m.timeline, %{ + last_read_id: m.last_read_id, + version: m.lock_version, + updated_at: NaiveDateTime.to_iso8601(m.updated_at) + }) + end) + end +end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 9b8dd3086..baff54151 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -243,7 +243,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} end direct_conversation_id = - with {_, true} <- {:include_id, opts[:with_direct_conversation_id]}, + with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]}, + {_, true} <- {:include_id, opts[:with_direct_conversation_id]}, {_, %User{} = for_user} <- {:for_user, opts[:for]}, %{data: %{"context" => context}} when is_binary(context) <- activity, %Conversation{} = conversation <- Conversation.get_for_ap_id(context), @@ -251,6 +252,9 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} Participation.for_user_and_conversation(for_user, conversation) do participation_id else + {:direct_conversation_id, participation_id} when is_integer(participation_id) -> + participation_id + _e -> nil end @@ -498,6 +502,6 @@ defp present?(nil), do: false defp present?(false), do: false defp present?(_), do: true - defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}), + defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}), do: id in pinned_activities end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index 3c26eb406..a400d1c8d 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -35,6 +35,13 @@ def init(%{qs: qs} = req, state) do {_, stream} <- List.keyfind(params, "stream", 0), {:ok, user} <- allow_request(stream, [access_token, sec_websocket]), topic when is_binary(topic) <- expand_topic(stream, params) do + req = + if sec_websocket do + :cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req) + else + req + end + {:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}} else {:error, code} -> diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 03c9a5027..fe71aca8c 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -202,9 +202,9 @@ def token_exchange( with {:ok, %User{} = user} <- Authenticator.get_user(conn), {:ok, app} <- Token.Utils.fetch_app(conn), {:auth_active, true} <- {:auth_active, User.auth_active?(user)}, - {:user_active, true} <- {:user_active, !user.info.deactivated}, + {:user_active, true} <- {:user_active, !user.deactivated}, {:password_reset_pending, false} <- - {:password_reset_pending, user.info.password_reset_pending}, + {:password_reset_pending, user.password_reset_pending}, {:ok, scopes} <- validate_scopes(app, params), {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, token} <- Token.exchange_token(app, auth) do diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex deleted file mode 100644 index 8e55b9f0b..000000000 --- a/lib/pleroma/web/ostatus/activity_representer.ex +++ /dev/null @@ -1,313 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.ActivityRepresenter do - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.OStatus.UserRepresenter - - require Logger - require Pleroma.Constants - - defp get_href(id) do - with %Object{data: %{"external_url" => external_url}} <- Object.get_cached_by_ap_id(id) do - external_url - else - _e -> id - end - end - - defp get_in_reply_to(activity) do - with %Object{data: %{"inReplyTo" => in_reply_to}} <- Object.normalize(activity) do - [ - {:"thr:in-reply-to", - [ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []} - ] - else - _ -> - [] - end - end - - defp get_mentions(to) do - Enum.map(to, fn id -> - cond do - # Special handling for the AP/Ostatus public collections - Pleroma.Constants.as_public() == id -> - {:link, - [ - rel: "mentioned", - "ostatus:object-type": "http://activitystrea.ms/schema/1.0/collection", - href: "http://activityschema.org/collection/public" - ], []} - - # Ostatus doesn't handle follower collections, ignore these. - Regex.match?(~r/^#{Pleroma.Web.base_url()}.+followers$/, id) -> - [] - - true -> - {:link, - [ - rel: "mentioned", - "ostatus:object-type": "http://activitystrea.ms/schema/1.0/person", - href: id - ], []} - end - end) - end - - defp get_links(%{local: true}, %{"id" => object_id}) do - h = fn str -> [to_charlist(str)] end - - [ - {:link, [type: ['application/atom+xml'], href: h.(object_id), rel: 'self'], []}, - {:link, [type: ['text/html'], href: h.(object_id), rel: 'alternate'], []} - ] - end - - defp get_links(%{local: false}, %{"external_url" => external_url}) do - h = fn str -> [to_charlist(str)] end - - [ - {:link, [type: ['text/html'], href: h.(external_url), rel: 'alternate'], []} - ] - end - - defp get_links(_activity, _object_data), do: [] - - defp get_emoji_links(emojis) do - Enum.map(emojis, fn {emoji, file} -> - {:link, [name: to_charlist(emoji), rel: 'emoji', href: to_charlist(file)], []} - end) - end - - def to_simple_form(activity, user, with_author \\ false) - - def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do - h = fn str -> [to_charlist(str)] end - - object = Object.normalize(activity) - - updated_at = object.data["published"] - inserted_at = object.data["published"] - - attachments = - Enum.map(object.data["attachment"] || [], fn attachment -> - url = hd(attachment["url"]) - - {:link, - [rel: 'enclosure', href: to_charlist(url["href"]), type: to_charlist(url["mediaType"])], - []} - end) - - in_reply_to = get_in_reply_to(activity) - author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] - mentions = activity.recipients |> get_mentions - - categories = - (object.data["tag"] || []) - |> Enum.map(fn tag -> - if is_binary(tag) do - {:category, [term: to_charlist(tag)], []} - else - nil - end - end) - |> Enum.filter(& &1) - - emoji_links = get_emoji_links(object.data["emoji"] || %{}) - - summary = - if object.data["summary"] do - [{:summary, [], h.(object.data["summary"])}] - else - [] - end - - [ - {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']}, - {:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']}, - # For notes, federate the object id. - {:id, h.(object.data["id"])}, - {:title, ['New note by #{user.nickname}']}, - {:content, [type: 'html'], h.(object.data["content"] |> String.replace(~r/[\n\r]/, ""))}, - {:published, h.(inserted_at)}, - {:updated, h.(updated_at)}, - {:"ostatus:conversation", [ref: h.(activity.data["context"])], - h.(activity.data["context"])}, - {:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []} - ] ++ - summary ++ - get_links(activity, object.data) ++ - categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links - end - - def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) do - h = fn str -> [to_charlist(str)] end - - updated_at = activity.data["published"] - inserted_at = activity.data["published"] - - author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] - mentions = activity.recipients |> get_mentions - - [ - {:"activity:verb", ['http://activitystrea.ms/schema/1.0/favorite']}, - {:id, h.(activity.data["id"])}, - {:title, ['New favorite by #{user.nickname}']}, - {:content, [type: 'html'], ['#{user.nickname} favorited something']}, - {:published, h.(inserted_at)}, - {:updated, h.(updated_at)}, - {:"activity:object", - [ - {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']}, - # For notes, federate the object id. - {:id, h.(activity.data["object"])} - ]}, - {:"ostatus:conversation", [ref: h.(activity.data["context"])], - h.(activity.data["context"])}, - {:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}, - {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}, - {:"thr:in-reply-to", [ref: to_charlist(activity.data["object"])], []} - ] ++ author ++ mentions - end - - def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_author) do - h = fn str -> [to_charlist(str)] end - - updated_at = activity.data["published"] - inserted_at = activity.data["published"] - - author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] - - retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"]) - retweeted_object = Object.normalize(retweeted_activity) - retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"]) - - retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true) - - mentions = - ([retweeted_user.ap_id] ++ activity.recipients) - |> Enum.uniq() - |> get_mentions() - - [ - {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, - {:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']}, - {:id, h.(activity.data["id"])}, - {:title, ['#{user.nickname} repeated a notice']}, - {:content, [type: 'html'], ['RT #{retweeted_object.data["content"]}']}, - {:published, h.(inserted_at)}, - {:updated, h.(updated_at)}, - {:"ostatus:conversation", [ref: h.(activity.data["context"])], - h.(activity.data["context"])}, - {:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}, - {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}, - {:"activity:object", retweeted_xml} - ] ++ mentions ++ author - end - - def to_simple_form(%{data: %{"type" => "Follow"}} = activity, user, with_author) do - h = fn str -> [to_charlist(str)] end - - updated_at = activity.data["published"] - inserted_at = activity.data["published"] - - author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] - - mentions = (activity.recipients || []) |> get_mentions - - [ - {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, - {:"activity:verb", ['http://activitystrea.ms/schema/1.0/follow']}, - {:id, h.(activity.data["id"])}, - {:title, ['#{user.nickname} started following #{activity.data["object"]}']}, - {:content, [type: 'html'], - ['#{user.nickname} started following #{activity.data["object"]}']}, - {:published, h.(inserted_at)}, - {:updated, h.(updated_at)}, - {:"activity:object", - [ - {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']}, - {:id, h.(activity.data["object"])}, - {:uri, h.(activity.data["object"])} - ]}, - {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []} - ] ++ mentions ++ author - end - - # Only undos of follow for now. Will need to get redone once there are more - def to_simple_form( - %{data: %{"type" => "Undo", "object" => %{"type" => "Follow"} = follow_activity}} = - activity, - user, - with_author - ) do - h = fn str -> [to_charlist(str)] end - - updated_at = activity.data["published"] - inserted_at = activity.data["published"] - - author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] - - mentions = (activity.recipients || []) |> get_mentions - follow_activity = Activity.normalize(follow_activity) - - [ - {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, - {:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']}, - {:id, h.(activity.data["id"])}, - {:title, ['#{user.nickname} stopped following #{follow_activity.data["object"]}']}, - {:content, [type: 'html'], - ['#{user.nickname} stopped following #{follow_activity.data["object"]}']}, - {:published, h.(inserted_at)}, - {:updated, h.(updated_at)}, - {:"activity:object", - [ - {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']}, - {:id, h.(follow_activity.data["object"])}, - {:uri, h.(follow_activity.data["object"])} - ]}, - {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []} - ] ++ mentions ++ author - end - - def to_simple_form(%{data: %{"type" => "Delete"}} = activity, user, with_author) do - h = fn str -> [to_charlist(str)] end - - updated_at = activity.data["published"] - inserted_at = activity.data["published"] - - author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] - - [ - {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, - {:"activity:verb", ['http://activitystrea.ms/schema/1.0/delete']}, - {:id, h.(activity.data["object"])}, - {:title, ['An object was deleted']}, - {:content, [type: 'html'], ['An object was deleted']}, - {:published, h.(inserted_at)}, - {:updated, h.(updated_at)} - ] ++ author - end - - def to_simple_form(_, _, _), do: nil - - def wrap_with_entry(simple_form) do - [ - { - :entry, - [ - xmlns: 'http://www.w3.org/2005/Atom', - "xmlns:thr": 'http://purl.org/syndication/thread/1.0', - "xmlns:activity": 'http://activitystrea.ms/spec/1.0/', - "xmlns:poco": 'http://portablecontacts.net/spec/1.0', - "xmlns:ostatus": 'http://ostatus.org/schema/1.0' - ], - simple_form - } - ] - end -end diff --git a/lib/pleroma/web/ostatus/feed_representer.ex b/lib/pleroma/web/ostatus/feed_representer.ex deleted file mode 100644 index b7b97e505..000000000 --- a/lib/pleroma/web/ostatus/feed_representer.ex +++ /dev/null @@ -1,66 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.FeedRepresenter do - alias Pleroma.User - alias Pleroma.Web.MediaProxy - alias Pleroma.Web.OStatus - alias Pleroma.Web.OStatus.ActivityRepresenter - alias Pleroma.Web.OStatus.UserRepresenter - - def to_simple_form(user, activities, _users) do - most_recent_update = - (List.first(activities) || user).updated_at - |> NaiveDateTime.to_iso8601() - - h = fn str -> [to_charlist(str)] end - - last_activity = List.last(activities) - - entries = - activities - |> Enum.map(fn activity -> - {:entry, ActivityRepresenter.to_simple_form(activity, user)} - end) - |> Enum.filter(fn {_, form} -> form end) - - [ - { - :feed, - [ - xmlns: 'http://www.w3.org/2005/Atom', - "xmlns:thr": 'http://purl.org/syndication/thread/1.0', - "xmlns:activity": 'http://activitystrea.ms/spec/1.0/', - "xmlns:poco": 'http://portablecontacts.net/spec/1.0', - "xmlns:ostatus": 'http://ostatus.org/schema/1.0' - ], - [ - {:id, h.(OStatus.feed_path(user))}, - {:title, ['#{user.nickname}\'s timeline']}, - {:updated, h.(most_recent_update)}, - {:logo, [to_charlist(User.avatar_url(user) |> MediaProxy.url())]}, - {:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []}, - {:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []}, - {:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'], - []}, - {:author, UserRepresenter.to_simple_form(user)} - ] ++ - if last_activity do - [ - {:link, - [ - rel: 'next', - href: - to_charlist(OStatus.feed_path(user)) ++ - '?max_id=' ++ to_charlist(last_activity.id), - type: 'application/atom+xml' - ], []} - ] - else - [] - end ++ entries - } - ] - end -end diff --git a/lib/pleroma/web/ostatus/handlers/delete_handler.ex b/lib/pleroma/web/ostatus/handlers/delete_handler.ex deleted file mode 100644 index b2f9f3946..000000000 --- a/lib/pleroma/web/ostatus/handlers/delete_handler.ex +++ /dev/null @@ -1,18 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.DeleteHandler do - require Logger - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.XML - - def handle_delete(entry, _doc \\ nil) do - with id <- XML.string_from_xpath("//id", entry), - %Object{} = object <- Object.normalize(id), - {:ok, delete} <- ActivityPub.delete(object, false) do - delete - end - end -end diff --git a/lib/pleroma/web/ostatus/handlers/follow_handler.ex b/lib/pleroma/web/ostatus/handlers/follow_handler.ex deleted file mode 100644 index 24513972e..000000000 --- a/lib/pleroma/web/ostatus/handlers/follow_handler.ex +++ /dev/null @@ -1,26 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.FollowHandler do - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.OStatus - alias Pleroma.Web.XML - - def handle(entry, doc) do - with {:ok, actor} <- OStatus.find_make_or_update_actor(doc), - id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry), - followed_uri when not is_nil(followed_uri) <- - XML.string_from_xpath("/entry/activity:object/id", entry), - {:ok, followed} <- OStatus.find_or_make_user(followed_uri), - {:locked, false} <- {:locked, followed.info.locked}, - {:ok, activity} <- ActivityPub.follow(actor, followed, id, false) do - User.follow(actor, followed) - {:ok, activity} - else - {:locked, true} -> - {:error, "It's not possible to follow locked accounts over OStatus"} - end - end -end diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex deleted file mode 100644 index 7fae14f7b..000000000 --- a/lib/pleroma/web/ostatus/handlers/note_handler.ex +++ /dev/null @@ -1,168 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.NoteHandler do - require Logger - require Pleroma.Constants - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Federator - alias Pleroma.Web.OStatus - alias Pleroma.Web.XML - - @doc """ - Get the context for this note. Uses this: - 1. The context of the parent activity - 2. The conversation reference in the ostatus xml - 3. A newly generated context id. - """ - def get_context(entry, in_reply_to) do - context = - (XML.string_from_xpath("//ostatus:conversation[1]", entry) || - XML.string_from_xpath("//ostatus:conversation[1]/@ref", entry) || "") - |> String.trim() - - with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(in_reply_to) do - context - else - _e -> - if String.length(context) > 0 do - context - else - Utils.generate_context_id() - end - end - end - - def get_people_mentions(entry) do - :xmerl_xpath.string( - '//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/person"]', - entry - ) - |> Enum.map(fn person -> XML.string_from_xpath("@href", person) end) - end - - def get_collection_mentions(entry) do - transmogrify = fn - "http://activityschema.org/collection/public" -> - Pleroma.Constants.as_public() - - group -> - group - end - - :xmerl_xpath.string( - '//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/collection"]', - entry - ) - |> Enum.map(fn collection -> XML.string_from_xpath("@href", collection) |> transmogrify.() end) - end - - def get_mentions(entry) do - (get_people_mentions(entry) ++ get_collection_mentions(entry)) - |> Enum.filter(& &1) - end - - def get_emoji(entry) do - try do - :xmerl_xpath.string('//link[@rel="emoji"]', entry) - |> Enum.reduce(%{}, fn emoji, acc -> - Map.put(acc, XML.string_from_xpath("@name", emoji), XML.string_from_xpath("@href", emoji)) - end) - rescue - _e -> nil - end - end - - def make_to_list(actor, mentions) do - [ - actor.follower_address - ] ++ mentions - end - - def add_external_url(note, entry) do - url = XML.string_from_xpath("//link[@rel='alternate' and @type='text/html']/@href", entry) - Map.put(note, "external_url", url) - end - - def fetch_replied_to_activity(entry, in_reply_to, options \\ []) do - with %Activity{} = activity <- Activity.get_create_by_object_ap_id(in_reply_to) do - activity - else - _e -> - with true <- Federator.allowed_incoming_reply_depth?(options[:depth]), - in_reply_to_href when not is_nil(in_reply_to_href) <- - XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry), - {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href, options) do - activity - else - _e -> nil - end - end - end - - # TODO: Clean this up a bit. - def handle_note(entry, doc \\ nil, options \\ []) do - with id <- XML.string_from_xpath("//id", entry), - activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id), - [author] <- :xmerl_xpath.string('//author[1]', doc), - {:ok, actor} <- OStatus.find_make_or_update_actor(author), - content_html <- OStatus.get_content(entry), - cw <- OStatus.get_cw(entry), - in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry), - options <- Keyword.put(options, :depth, (options[:depth] || 0) + 1), - in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to, options), - in_reply_to_object <- - (in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil, - in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to, - attachments <- OStatus.get_attachments(entry), - context <- get_context(entry, in_reply_to), - tags <- OStatus.get_tags(entry), - mentions <- get_mentions(entry), - to <- make_to_list(actor, mentions), - date <- XML.string_from_xpath("//published", entry), - unlisted <- XML.string_from_xpath("//mastodon:scope", entry) == "unlisted", - cc <- if(unlisted, do: [Pleroma.Constants.as_public()], else: []), - note <- - CommonAPI.Utils.make_note_data( - actor.ap_id, - to, - context, - content_html, - attachments, - in_reply_to_activity, - [], - cw - ), - note <- note |> Map.put("id", id) |> Map.put("tag", tags), - note <- note |> Map.put("published", date), - note <- note |> Map.put("emoji", get_emoji(entry)), - note <- add_external_url(note, entry), - note <- note |> Map.put("cc", cc), - # TODO: Handle this case in make_note_data - note <- - if( - in_reply_to && !in_reply_to_activity, - do: note |> Map.put("inReplyTo", in_reply_to), - else: note - ) do - ActivityPub.create(%{ - to: to, - actor: actor, - context: context, - object: note, - published: date, - local: false, - additional: %{"cc" => cc} - }) - else - %Activity{} = activity -> {:ok, activity} - e -> {:error, e} - end - end -end diff --git a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex deleted file mode 100644 index 2062432e3..000000000 --- a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex +++ /dev/null @@ -1,22 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.UnfollowHandler do - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.OStatus - alias Pleroma.Web.XML - - def handle(entry, doc) do - with {:ok, actor} <- OStatus.find_make_or_update_actor(doc), - id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry), - followed_uri when not is_nil(followed_uri) <- - XML.string_from_xpath("/entry/activity:object/id", entry), - {:ok, followed} <- OStatus.find_or_make_user(followed_uri), - {:ok, activity} <- ActivityPub.unfollow(actor, followed, id, false) do - User.unfollow(actor, followed) - {:ok, activity} - end - end -end diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex deleted file mode 100644 index 5de1ceef3..000000000 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ /dev/null @@ -1,395 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus do - import Pleroma.Web.XML - require Logger - - alias Pleroma.Activity - alias Pleroma.HTTP - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.OStatus.DeleteHandler - alias Pleroma.Web.OStatus.FollowHandler - alias Pleroma.Web.OStatus.NoteHandler - alias Pleroma.Web.OStatus.UnfollowHandler - alias Pleroma.Web.WebFinger - alias Pleroma.Web.Websub - - def is_representable?(%Activity{} = activity) do - object = Object.normalize(activity) - - cond do - is_nil(object) -> - false - - Visibility.is_public?(activity) && object.data["type"] == "Note" -> - true - - true -> - false - end - end - - def feed_path(user), do: "#{user.ap_id}/feed.atom" - - def pubsub_path(user), do: "#{Web.base_url()}/push/hub/#{user.nickname}" - - def salmon_path(user), do: "#{user.ap_id}/salmon" - - def remote_follow_path, do: "#{Web.base_url()}/ostatus_subscribe?acct={uri}" - - def handle_incoming(xml_string, options \\ []) do - with doc when doc != :error <- parse_document(xml_string) do - with {:ok, actor_user} <- find_make_or_update_actor(doc), - do: Pleroma.Instances.set_reachable(actor_user.ap_id) - - entries = :xmerl_xpath.string('//entry', doc) - - activities = - Enum.map(entries, fn entry -> - {:xmlObj, :string, object_type} = - :xmerl_xpath.string('string(/entry/activity:object-type[1])', entry) - - {:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry) - Logger.debug("Handling #{verb}") - - try do - case verb do - 'http://activitystrea.ms/schema/1.0/delete' -> - with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity - - 'http://activitystrea.ms/schema/1.0/follow' -> - with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity - - 'http://activitystrea.ms/schema/1.0/unfollow' -> - with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity - - 'http://activitystrea.ms/schema/1.0/share' -> - with {:ok, activity, retweeted_activity} <- handle_share(entry, doc), - do: [activity, retweeted_activity] - - 'http://activitystrea.ms/schema/1.0/favorite' -> - with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc), - do: [activity, favorited_activity] - - _ -> - case object_type do - 'http://activitystrea.ms/schema/1.0/note' -> - with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options), - do: activity - - 'http://activitystrea.ms/schema/1.0/comment' -> - with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options), - do: activity - - _ -> - Logger.error("Couldn't parse incoming document") - nil - end - end - rescue - e -> - Logger.error("Error occured while handling activity") - Logger.error(xml_string) - Logger.error(inspect(e)) - nil - end - end) - |> Enum.filter(& &1) - - {:ok, activities} - else - _e -> {:error, []} - end - end - - def make_share(entry, doc, retweeted_activity) do - with {:ok, actor} <- find_make_or_update_actor(doc), - %Object{} = object <- Object.normalize(retweeted_activity), - id when not is_nil(id) <- string_from_xpath("/entry/id", entry), - {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do - {:ok, activity} - end - end - - def handle_share(entry, doc) do - with {:ok, retweeted_activity} <- get_or_build_object(entry), - {:ok, activity} <- make_share(entry, doc, retweeted_activity) do - {:ok, activity, retweeted_activity} - else - e -> {:error, e} - end - end - - def make_favorite(entry, doc, favorited_activity) do - with {:ok, actor} <- find_make_or_update_actor(doc), - %Object{} = object <- Object.normalize(favorited_activity), - id when not is_nil(id) <- string_from_xpath("/entry/id", entry), - {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do - {:ok, activity} - end - end - - def get_or_build_object(entry) do - with {:ok, activity} <- get_or_try_fetching(entry) do - {:ok, activity} - else - _e -> - with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do - NoteHandler.handle_note(object, object) - end - end - end - - def get_or_try_fetching(entry) do - Logger.debug("Trying to get entry from db") - - with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry), - %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do - {:ok, activity} - else - _ -> - Logger.debug("Couldn't get, will try to fetch") - - with href when not is_nil(href) <- - string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry), - {:ok, [favorited_activity]} <- fetch_activity_from_url(href) do - {:ok, favorited_activity} - else - e -> Logger.debug("Couldn't find href: #{inspect(e)}") - end - end - end - - def handle_favorite(entry, doc) do - with {:ok, favorited_activity} <- get_or_try_fetching(entry), - {:ok, activity} <- make_favorite(entry, doc, favorited_activity) do - {:ok, activity, favorited_activity} - else - e -> {:error, e} - end - end - - def get_attachments(entry) do - :xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry) - |> Enum.map(fn enclosure -> - with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure), - type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do - %{ - "type" => "Attachment", - "url" => [ - %{ - "type" => "Link", - "mediaType" => type, - "href" => href - } - ] - } - end - end) - |> Enum.filter(& &1) - end - - @doc """ - Gets the content from a an entry. - """ - def get_content(entry) do - string_from_xpath("//content", entry) - end - - @doc """ - Get the cw that mastodon uses. - """ - def get_cw(entry) do - case string_from_xpath("/*/summary", entry) do - cw when not is_nil(cw) -> cw - _ -> nil - end - end - - def get_tags(entry) do - :xmerl_xpath.string('//category', entry) - |> Enum.map(fn category -> string_from_xpath("/category/@term", category) end) - |> Enum.filter(& &1) - |> Enum.map(&String.downcase/1) - end - - def maybe_update(doc, user) do - case string_from_xpath("//author[1]/ap_enabled", doc) do - "true" -> - Transmogrifier.upgrade_user_from_ap_id(user.ap_id) - - _ -> - maybe_update_ostatus(doc, user) - end - end - - def maybe_update_ostatus(doc, user) do - old_data = Map.take(user, [:bio, :avatar, :name]) - - with false <- user.local, - avatar <- make_avatar_object(doc), - bio <- string_from_xpath("//author[1]/summary", doc), - name <- string_from_xpath("//author[1]/poco:displayName", doc), - new_data <- %{ - avatar: avatar || old_data.avatar, - name: name || old_data.name, - bio: bio || old_data.bio - }, - false <- new_data == old_data do - change = Ecto.Changeset.change(user, new_data) - User.update_and_set_cache(change) - else - _ -> - {:ok, user} - end - end - - def find_make_or_update_actor(doc) do - uri = string_from_xpath("//author/uri[1]", doc) - - with {:ok, %User{} = user} <- find_or_make_user(uri), - {:ap_enabled, false} <- {:ap_enabled, User.ap_enabled?(user)} do - maybe_update(doc, user) - else - {:ap_enabled, true} -> - {:error, :invalid_protocol} - - _ -> - {:error, :unknown_user} - end - end - - @spec find_or_make_user(String.t()) :: {:ok, User.t()} - def find_or_make_user(uri) do - case User.get_by_ap_id(uri) do - %User{} = user -> {:ok, user} - _ -> make_user(uri) - end - end - - @spec make_user(String.t(), boolean()) :: {:ok, User.t()} | {:error, any()} - def make_user(uri, update \\ false) do - with {:ok, info} <- gather_user_info(uri) do - with false <- update, - %User{} = user <- User.get_cached_by_ap_id(info["uri"]) do - {:ok, user} - else - _e -> User.insert_or_update_user(build_user_data(info)) - end - end - end - - defp build_user_data(info) do - %{ - name: info["name"], - nickname: info["nickname"] <> "@" <> info["host"], - ap_id: info["uri"], - info: info, - avatar: info["avatar"], - bio: info["bio"] - } - end - - # TODO: Just takes the first one for now. - def make_avatar_object(author_doc, rel \\ "avatar") do - href = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@href", author_doc) - type = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@type", author_doc) - - if href do - %{ - "type" => "Image", - "url" => [%{"type" => "Link", "mediaType" => type, "href" => href}] - } - else - nil - end - end - - @spec gather_user_info(String.t()) :: {:ok, map()} | {:error, any()} - def gather_user_info(username) do - with {:ok, webfinger_data} <- WebFinger.finger(username), - {:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do - data = - webfinger_data - |> Map.merge(feed_data) - |> Map.put("fqn", username) - - {:ok, data} - else - e -> - Logger.debug(fn -> "Couldn't gather info for #{username}" end) - {:error, e} - end - end - - # Regex-based 'parsing' so we don't have to pull in a full html parser - # It's a hack anyway. Maybe revisit this in the future - @mastodon_regex ~r// - @gs_regex ~r// - @gs_classic_regex ~r// - def get_atom_url(body) do - cond do - Regex.match?(@mastodon_regex, body) -> - [[_, match]] = Regex.scan(@mastodon_regex, body) - {:ok, match} - - Regex.match?(@gs_regex, body) -> - [[_, match]] = Regex.scan(@gs_regex, body) - {:ok, match} - - Regex.match?(@gs_classic_regex, body) -> - [[_, match]] = Regex.scan(@gs_classic_regex, body) - {:ok, match} - - true -> - Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end) - {:error, "Couldn't find the Atom link"} - end - end - - def fetch_activity_from_atom_url(url, options \\ []) do - with true <- String.starts_with?(url, "http"), - {:ok, %{body: body, status: code}} when code in 200..299 <- - HTTP.get(url, [{:Accept, "application/atom+xml"}]) do - Logger.debug("Got document from #{url}, handling...") - handle_incoming(body, options) - else - e -> - Logger.debug("Couldn't get #{url}: #{inspect(e)}") - e - end - end - - def fetch_activity_from_html_url(url, options \\ []) do - Logger.debug("Trying to fetch #{url}") - - with true <- String.starts_with?(url, "http"), - {:ok, %{body: body}} <- HTTP.get(url, []), - {:ok, atom_url} <- get_atom_url(body) do - fetch_activity_from_atom_url(atom_url, options) - else - e -> - Logger.debug("Couldn't get #{url}: #{inspect(e)}") - e - end - end - - def fetch_activity_from_url(url, options \\ []) do - with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url, options) do - {:ok, activities} - else - _e -> fetch_activity_from_html_url(url, options) - end - rescue - e -> - Logger.debug("Couldn't get #{url}: #{inspect(e)}") - {:error, "Couldn't get #{url}: #{inspect(e)}"} - end -end diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 20f2d9ddc..6958519de 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -13,19 +13,14 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Endpoint - alias Pleroma.Web.Federator alias Pleroma.Web.Metadata.PlayerView - alias Pleroma.Web.OStatus.ActivityRepresenter alias Pleroma.Web.Router - alias Pleroma.Web.XML plug( Pleroma.Plugs.RateLimiter, {:ap_routes, params: ["uuid"]} when action in [:object, :activity] ) - plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming]) - plug( Pleroma.Plugs.SetFormatPlug when action in [:object, :activity, :notice] @@ -33,32 +28,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do action_fallback(:errors) - defp decode_or_retry(body) do - with {:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body), - {:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do - {:ok, doc} - else - _e -> - with [decoded | _] <- Pleroma.Web.Salmon.decode(body), - doc <- XML.parse_document(decoded), - uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc), - {:ok, _} <- Pleroma.Web.OStatus.make_user(uri, true), - {:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body), - {:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do - {:ok, doc} - end - end - end - - def salmon_incoming(conn, _) do - {:ok, body, _conn} = read_body(conn) - {:ok, doc} = decode_or_retry(body) - - Federator.incoming_doc(doc) - - send_resp(conn, 200, "") - end - def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid}) when format in ["json", "activity+json"] do ActivityPubController.call(conn, :object) @@ -179,23 +148,10 @@ defp represent_activity( |> render("object.json", %{object: object}) end - defp represent_activity(_conn, "activity+json", _, _) do + defp represent_activity(_conn, _, _, _) do {:error, :not_found} end - defp represent_activity(conn, _, activity, user) do - response = - activity - |> ActivityRepresenter.to_simple_form(user, true) - |> ActivityRepresenter.wrap_with_entry() - |> :xmerl.export_simple(:xmerl_xml) - |> to_string - - conn - |> put_resp_content_type("application/atom+xml") - |> send_resp(200, response) - end - def errors(conn, {:error, :not_found}) do render_error(conn, :not_found, "Not found") end diff --git a/lib/pleroma/web/ostatus/user_representer.ex b/lib/pleroma/web/ostatus/user_representer.ex deleted file mode 100644 index 852be6eb4..000000000 --- a/lib/pleroma/web/ostatus/user_representer.ex +++ /dev/null @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.UserRepresenter do - alias Pleroma.User - - def to_simple_form(user) do - ap_id = to_charlist(user.ap_id) - nickname = to_charlist(user.nickname) - name = to_charlist(user.name) - bio = to_charlist(user.bio) - avatar_url = to_charlist(User.avatar_url(user)) - - banner = - if banner_url = User.banner_url(user) do - [{:link, [rel: 'header', href: banner_url], []}] - else - [] - end - - ap_enabled = - if user.local do - [{:ap_enabled, ['true']}] - else - [] - end - - [ - {:id, [ap_id]}, - {:"activity:object", ['http://activitystrea.ms/schema/1.0/person']}, - {:uri, [ap_id]}, - {:"poco:preferredUsername", [nickname]}, - {:"poco:displayName", [name]}, - {:"poco:note", [bio]}, - {:summary, [bio]}, - {:name, [nickname]}, - {:link, [rel: 'avatar', href: avatar_url], []} - ] ++ banner ++ ap_enabled - end -end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 9012e2175..db6faac83 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -80,9 +80,7 @@ def update_avatar(%{assigns: %{user: user}} = conn, params) do @doc "PATCH /api/v1/pleroma/accounts/update_banner" def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do - new_info = %{"banner" => %{}} - - with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do + with {:ok, user} <- User.update_banner(user, %{}) do CommonAPI.update(user) json(conn, %{url: nil}) end @@ -90,8 +88,7 @@ def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do def update_banner(%{assigns: %{user: user}} = conn, params) do with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), - new_info <- %{"banner" => object.data}, - {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do + {:ok, user} <- User.update_banner(user, object.data) do CommonAPI.update(user) %{"url" => [%{"href" => href} | _]} = object.data @@ -101,17 +98,14 @@ def update_banner(%{assigns: %{user: user}} = conn, params) do @doc "PATCH /api/v1/pleroma/accounts/update_background" def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do - new_info = %{"background" => %{}} - - with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do + with {:ok, _user} <- User.update_background(user, %{}) do json(conn, %{url: nil}) end end def update_background(%{assigns: %{user: user}} = conn, params) do with {:ok, object} <- ActivityPub.upload(params, type: :background), - new_info <- %{"background" => object.data}, - {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do + {:ok, _user} <- User.update_background(user, object.data) do %{"url" => [%{"href" => href} | _]} = object.data json(conn, %{url: href}) @@ -119,7 +113,7 @@ def update_background(%{assigns: %{user: user}} = conn, params) do end @doc "GET /api/v1/pleroma/accounts/:id/favourites" - def favourites(%{assigns: %{account: %{info: %{hide_favorites: true}}}} = conn, _params) do + def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do render_error(conn, :forbidden, "Can't get favorites") end @@ -132,7 +126,7 @@ def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do recipients = if for_user do - [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following] + [Pleroma.Constants.as_public()] ++ [for_user.ap_id | User.following(for_user)] else [Pleroma.Constants.as_public()] end diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index d71d72dd5..8cf552b7e 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -24,9 +24,7 @@ def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), # Reject if not an image %{type: "image"} = attachment <- render_attachment(object) do - # Sure! - # Save to the user's info - {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, attachment)) + {:ok, _user} = User.mascot_update(user, attachment) json(conn, attachment) else diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index 6db213475..8fed3f5bb 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -125,6 +125,15 @@ def update_conversation( end end + def read_conversations(%{assigns: %{user: user}} = conn, _params) do + with {:ok, _, participations} <- Participation.mark_all_as_read(user) do + conn + |> add_link_headers(participations) + |> put_view(ConversationView) + |> render("participations.json", participations: participations, for: user) + end + end + def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do with {:ok, notification} <- Notification.read_one(user, notification_id) do conn diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 35d3ff07c..dd445e8bf 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -125,6 +125,10 @@ def format_body( end end + def format_title(%{activity: %{data: %{"directMessage" => true}}}) do + "New Direct Message" + end + def format_title(%{activity: %{data: %{"type" => type}}}) do case type do "Create" -> "New Mention" diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex index d376e2069..16b1a53d2 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -25,13 +25,13 @@ def parse(url) when is_binary(url) do def parse(_), do: {:error, "No URL provided"} defp parse_url(url) do - {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options) - - data = - Floki.attribute(html, "link[rel~=me]", "href") ++ - Floki.attribute(html, "a[rel~=me]", "href") - - {:ok, data} + with {:ok, %Tesla.Env{body: html, status: status}} when status in 200..299 <- + Pleroma.HTTP.get(url, [], adapter: @hackney_options), + data <- + Floki.attribute(html, "link[rel~=me]", "href") ++ + Floki.attribute(html, "a[rel~=me]", "href") do + {:ok, data} + end rescue e -> {:error, "Parsing error: #{inspect(e)}"} end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a2e9d084f..7349f4cda 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -137,11 +137,14 @@ defmodule Pleroma.Web.Router do delete("/users", AdminAPIController, :user_delete) post("/users", AdminAPIController, :users_create) patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) + patch("/users/activate", AdminAPIController, :user_activate) + patch("/users/deactivate", AdminAPIController, :user_deactivate) put("/users/tag", AdminAPIController, :tag_users) delete("/users/tag", AdminAPIController, :untag_users) get("/users/:nickname/permission_group", AdminAPIController, :right_get) get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get) + post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add) delete( @@ -150,8 +153,15 @@ defmodule Pleroma.Web.Router do :right_delete ) - put("/users/:nickname/activation_status", AdminAPIController, :set_activation_status) + post("/users/permission_group/:permission_group", AdminAPIController, :right_add_multiple) + delete( + "/users/permission_group/:permission_group", + AdminAPIController, + :right_delete_multiple + ) + + get("/relay", AdminAPIController, :relay_list) post("/relay", AdminAPIController, :relay_follow) delete("/relay", AdminAPIController, :relay_unfollow) @@ -161,7 +171,7 @@ defmodule Pleroma.Web.Router do post("/users/email_invite", AdminAPIController, :email_invite) get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) - patch("/users/:nickname/force_password_reset", AdminAPIController, :force_password_reset) + patch("/users/force_password_reset", AdminAPIController, :force_password_reset) get("/users", AdminAPIController, :list_users) get("/users/:nickname", AdminAPIController, :user_show) @@ -262,6 +272,7 @@ defmodule Pleroma.Web.Router do get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) get("/conversations/:id", PleromaAPIController, :conversation) + post("/conversations/read", PleromaAPIController, :read_conversations) end scope [] do @@ -402,6 +413,9 @@ defmodule Pleroma.Web.Router do get("/push/subscription", SubscriptionController, :get) put("/push/subscription", SubscriptionController, :update) delete("/push/subscription", SubscriptionController, :delete) + + get("/markers", MarkerController, :index) + post("/markers", MarkerController, :upsert) end scope "/api/web", Pleroma.Web do @@ -507,11 +521,6 @@ defmodule Pleroma.Web.Router do get("/users/:nickname/feed", Feed.FeedController, :feed) get("/users/:nickname", Feed.FeedController, :feed_redirect) - post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming) - post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request) - get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation) - post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming) - get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) end @@ -594,6 +603,12 @@ defmodule Pleroma.Web.Router do get("/:version", Nodeinfo.NodeinfoController, :nodeinfo) end + scope "/", Pleroma.Web do + pipe_through(:api) + + get("/web/manifest.json", MastoFEController, :manifest) + end + scope "/", Pleroma.Web do pipe_through(:mastodon_html) diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex deleted file mode 100644 index 0ffe903cd..000000000 --- a/lib/pleroma/web/salmon/salmon.ex +++ /dev/null @@ -1,254 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Salmon do - @behaviour Pleroma.Web.Federator.Publisher - - use Bitwise - - alias Pleroma.Activity - alias Pleroma.HTTP - alias Pleroma.Instances - alias Pleroma.Keys - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.Federator.Publisher - alias Pleroma.Web.OStatus - alias Pleroma.Web.OStatus.ActivityRepresenter - alias Pleroma.Web.XML - - require Logger - - def decode(salmon) do - doc = XML.parse_document(salmon) - - {:xmlObj, :string, data} = :xmerl_xpath.string('string(//me:data[1])', doc) - {:xmlObj, :string, sig} = :xmerl_xpath.string('string(//me:sig[1])', doc) - {:xmlObj, :string, alg} = :xmerl_xpath.string('string(//me:alg[1])', doc) - {:xmlObj, :string, encoding} = :xmerl_xpath.string('string(//me:encoding[1])', doc) - {:xmlObj, :string, type} = :xmerl_xpath.string('string(//me:data[1]/@type)', doc) - - {:ok, data} = Base.url_decode64(to_string(data), ignore: :whitespace) - {:ok, sig} = Base.url_decode64(to_string(sig), ignore: :whitespace) - alg = to_string(alg) - encoding = to_string(encoding) - type = to_string(type) - - [data, type, encoding, alg, sig] - end - - def fetch_magic_key(salmon) do - with [data, _, _, _, _] <- decode(salmon), - doc <- XML.parse_document(data), - uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc), - {:ok, public_key} <- User.get_public_key_for_ap_id(uri), - magic_key <- encode_key(public_key) do - {:ok, magic_key} - end - end - - def decode_and_validate(magickey, salmon) do - [data, type, encoding, alg, sig] = decode(salmon) - - signed_text = - [data, type, encoding, alg] - |> Enum.map(&Base.url_encode64/1) - |> Enum.join(".") - - key = decode_key(magickey) - - verify = :public_key.verify(signed_text, :sha256, sig, key) - - if verify do - {:ok, data} - else - :error - end - end - - def decode_key("RSA." <> magickey) do - make_integer = fn bin -> - list = :erlang.binary_to_list(bin) - Enum.reduce(list, 0, fn el, acc -> acc <<< 8 ||| el end) - end - - [modulus, exponent] = - magickey - |> String.split(".") - |> Enum.map(fn n -> Base.url_decode64!(n, padding: false) end) - |> Enum.map(make_integer) - - {:RSAPublicKey, modulus, exponent} - end - - def encode_key({:RSAPublicKey, modulus, exponent}) do - modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64() - exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64() - - "RSA.#{modulus_enc}.#{exponent_enc}" - end - - def encode(private_key, doc) do - type = "application/atom+xml" - encoding = "base64url" - alg = "RSA-SHA256" - - signed_text = - [doc, type, encoding, alg] - |> Enum.map(&Base.url_encode64/1) - |> Enum.join(".") - - signature = - signed_text - |> :public_key.sign(:sha256, private_key) - |> to_string - |> Base.url_encode64() - - doc_base64 = - doc - |> Base.url_encode64() - - # Don't need proper xml building, these strings are safe to leave unescaped - salmon = """ - - - #{doc_base64} - #{encoding} - #{alg} - #{signature} - - """ - - {:ok, salmon} - end - - def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do - cc = Map.get(data, "cc", []) - - bcc = - data - |> Map.get("bcc", []) - |> Enum.reduce([], fn ap_id, bcc -> - case Pleroma.List.get_by_ap_id(ap_id) do - %Pleroma.List{user_id: ^user_id} = list -> - {:ok, following} = Pleroma.List.get_following(list) - bcc ++ Enum.map(following, & &1.ap_id) - - _ -> - bcc - end - end) - - [to, cc, bcc] - |> Enum.concat() - |> Enum.map(&User.get_cached_by_ap_id/1) - |> Enum.filter(fn user -> user && !user.local end) - end - - @doc "Pushes an activity to remote account." - def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params), - do: publish_one(Map.put(params, :recipient, salmon)) - - def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do - with {:ok, %{status: code}} when code in 200..299 <- - HTTP.post( - url, - feed, - [{"Content-Type", "application/magic-envelope+xml"}] - ) do - if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], - do: Instances.set_reachable(url) - - Logger.debug(fn -> "Pushed to #{url}, code #{code}" end) - {:ok, code} - else - e -> - unless params[:unreachable_since], do: Instances.set_reachable(url) - Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end) - {:error, "Unreachable instance"} - end - end - - def publish_one(%{recipient_id: recipient_id} = params) do - recipient = User.get_cached_by_id(recipient_id) - - params - |> Map.delete(:recipient_id) - |> Map.put(:recipient, recipient) - |> publish_one() - end - - def publish_one(_), do: :noop - - @supported_activities [ - "Create", - "Follow", - "Like", - "Announce", - "Undo", - "Delete" - ] - - def is_representable?(%Activity{data: %{"type" => type}} = activity) - when type in @supported_activities, - do: Visibility.is_public?(activity) - - def is_representable?(_), do: false - - @doc """ - Publishes an activity to remote accounts - """ - @spec publish(User.t(), Pleroma.Activity.t()) :: none - def publish(user, activity) - - def publish(%{keys: keys} = user, %{data: %{"type" => type}} = activity) - when type in @supported_activities do - feed = ActivityRepresenter.to_simple_form(activity, user, true) - - if feed do - feed = - ActivityRepresenter.wrap_with_entry(feed) - |> :xmerl.export_simple(:xmerl_xml) - |> to_string - - {:ok, private, _} = Keys.keys_from_pem(keys) - {:ok, feed} = encode(private, feed) - - remote_users = remote_users(user, activity) - - salmon_urls = Enum.map(remote_users, & &1.info.salmon) - reachable_urls_metadata = Instances.filter_reachable(salmon_urls) - reachable_urls = Map.keys(reachable_urls_metadata) - - remote_users - |> Enum.filter(&(&1.info.salmon in reachable_urls)) - |> Enum.each(fn remote_user -> - Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end) - - Publisher.enqueue_one(__MODULE__, %{ - recipient_id: remote_user.id, - feed: feed, - unreachable_since: reachable_urls_metadata[remote_user.info.salmon] - }) - end) - end - end - - def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end) - - def gather_webfinger_links(%User{} = user) do - {:ok, _private, public} = Keys.keys_from_pem(user.keys) - magic_key = encode_key(public) - - [ - %{"rel" => "salmon", "href" => OStatus.salmon_path(user)}, - %{ - "rel" => "magic-public-key", - "href" => "data:application/magic-public-key,#{magic_key}" - } - ] - end - - def gather_nodeinfo_protocol_names, do: [] -end diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index 8cf719277..2fc7ac8cf 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -49,7 +49,7 @@ defp handle_should_send(:test) do end end - defp handle_should_send(_) do - true - end + defp handle_should_send(:benchmark), do: false + + defp handle_should_send(_), do: true end diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex index 0ea224874..33b24840d 100644 --- a/lib/pleroma/web/streamer/worker.ex +++ b/lib/pleroma/web/streamer/worker.ex @@ -129,14 +129,14 @@ defp do_stream(%{topic: topic, item: item}) do end defp should_send?(%User{} = user, %Activity{} = item) do - blocks = user.info.blocks || [] - mutes = user.info.mutes || [] - reblog_mutes = user.info.muted_reblogs || [] + blocks = user.blocks || [] + mutes = user.mutes || [] + reblog_mutes = user.muted_reblogs || [] recipient_blocks = MapSet.new(blocks ++ mutes) recipients = MapSet.new(item.recipients) - domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks) + domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) - with parent when not is_nil(parent) <- Object.normalize(item), + 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 <- MapSet.disjoint?(recipients, recipient_blocks), @@ -212,7 +212,7 @@ def push_to_socket(topics, topic, item) do end @spec thread_containment(Activity.t(), User.t()) :: boolean() - defp thread_containment(_activity, %User{info: %{skip_thread_containment: true}}), do: true + defp thread_containment(_activity, %User{skip_thread_containment: true}), do: true defp thread_containment(activity, user) do if Config.get([:instance, :skip_thread_containment]) do diff --git a/lib/pleroma/web/templates/feed/feed/feed.xml.eex b/lib/pleroma/web/templates/feed/feed/feed.xml.eex index fbfdc46b5..45df9dc09 100644 --- a/lib/pleroma/web/templates/feed/feed/feed.xml.eex +++ b/lib/pleroma/web/templates/feed/feed/feed.xml.eex @@ -10,8 +10,6 @@ <%= @user.nickname <> "'s timeline" %> <%= most_recent_update(@activities, @user) %> <%= logo(@user) %> - - <%= render @view_module, "_author.xml", assigns %> diff --git a/lib/pleroma/web/templates/masto_fe/index.html.eex b/lib/pleroma/web/templates/masto_fe/index.html.eex index feff36fae..c330960fa 100644 --- a/lib/pleroma/web/templates/masto_fe/index.html.eex +++ b/lib/pleroma/web/templates/masto_fe/index.html.eex @@ -4,9 +4,13 @@ -<%= Pleroma.Config.get([:instance, :name]) %> +<%= Config.get([:instance, :name]) %> + + + + diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index bf5a6ae42..39f10c49f 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -20,11 +20,12 @@ defmodule Pleroma.Web.TwitterAPI.Controller do action_fallback(:errors) def confirm_email(conn, %{"user_id" => uid, "token" => token}) do - new_info = [need_confirmation: false] - - with %User{info: info} = user <- User.get_cached_by_id(uid), - true <- user.local and info.confirmation_pending and info.confirmation_token == token, - {:ok, _} <- User.update_info(user, &User.Info.confirmation_changeset(&1, new_info)) do + with %User{} = user <- User.get_cached_by_id(uid), + true <- user.local and user.confirmation_pending and user.confirmation_token == token, + {:ok, _} <- + user + |> User.confirmation_changeset(need_confirmation: false) + |> User.update_and_set_cache() do redirect(conn, to: "/") end end diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex index 21b086d4c..c39b7f095 100644 --- a/lib/pleroma/web/views/masto_fe_view.ex +++ b/lib/pleroma/web/views/masto_fe_view.ex @@ -61,12 +61,12 @@ def initial_state(token, user, custom_emojis) do }, poll_limits: Config.get([:instance, :poll_limits]), rights: %{ - delete_others_notice: present?(user.info.is_moderator), - admin: present?(user.info.is_admin) + delete_others_notice: present?(user.is_moderator), + admin: present?(user.is_admin) }, compose: %{ me: "#{user.id}", - default_privacy: user.info.default_scope, + default_privacy: user.default_scope, default_sensitive: false, allow_content_types: Config.get([:instance, :allowed_post_formats]) }, @@ -86,7 +86,7 @@ def initial_state(token, user, custom_emojis) do "video\/mp4" ] }, - settings: user.info.settings || @default_settings, + settings: user.settings || @default_settings, push_subscription: nil, accounts: %{user.id => render(AccountView, "show.json", user: user, for: user)}, custom_emojis: render(CustomEmojiView, "index.json", custom_emojis: custom_emojis), @@ -99,4 +99,23 @@ def initial_state(token, user, custom_emojis) do defp present?(nil), do: false defp present?(false), do: false defp present?(_), do: true + + def render("manifest.json", _params) do + %{ + name: Config.get([:instance, :name]), + description: Config.get([:instance, :description]), + icons: Config.get([:manifest, :icons]), + theme_color: Config.get([:manifest, :theme_color]), + background_color: Config.get([:manifest, :background_color]), + display: "standalone", + scope: Pleroma.Web.base_url(), + start_url: masto_fe_path(Pleroma.Web.Endpoint, :index, ["getting-started"]), + categories: [ + "social" + ], + serviceworker: %{ + src: "/sw.js" + } + } + end end diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index ecb39ee50..b4cc80179 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -108,7 +108,6 @@ defp webfinger_from_xml(doc) do doc ), subject <- XML.string_from_xpath("//Subject", doc), - salmon <- XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc), subscribe_address <- XML.string_from_xpath( ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}, @@ -123,7 +122,6 @@ defp webfinger_from_xml(doc) do "magic_key" => magic_key, "topic" => topic, "subject" => subject, - "salmon" => salmon, "subscribe_address" => subscribe_address, "ap_id" => ap_id } @@ -148,16 +146,6 @@ defp webfinger_from_json(doc) do {"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} -> Map.put(data, "ap_id", link["href"]) - {_, "magic-public-key"} -> - "data:application/magic-public-key," <> magic_key = link["href"] - Map.put(data, "magic_key", magic_key) - - {"application/atom+xml", "http://schemas.google.com/g/2010#updates-from"} -> - Map.put(data, "topic", link["href"]) - - {_, "salmon"} -> - Map.put(data, "salmon", link["href"]) - {_, "http://ostatus.org/schema/1.0/subscribe"} -> Map.put(data, "subscribe_address", link["template"]) diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex deleted file mode 100644 index b61f388b8..000000000 --- a/lib/pleroma/web/websub/websub.ex +++ /dev/null @@ -1,332 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Websub do - alias Ecto.Changeset - alias Pleroma.Activity - alias Pleroma.HTTP - alias Pleroma.Instances - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.ActivityPub.Visibility - alias Pleroma.Web.Endpoint - alias Pleroma.Web.Federator - alias Pleroma.Web.Federator.Publisher - alias Pleroma.Web.OStatus - alias Pleroma.Web.OStatus.FeedRepresenter - alias Pleroma.Web.Router.Helpers - alias Pleroma.Web.Websub.WebsubClientSubscription - alias Pleroma.Web.Websub.WebsubServerSubscription - alias Pleroma.Web.XML - require Logger - - import Ecto.Query - - @behaviour Pleroma.Web.Federator.Publisher - - def verify(subscription, getter \\ &HTTP.get/3) do - challenge = Base.encode16(:crypto.strong_rand_bytes(8)) - lease_seconds = NaiveDateTime.diff(subscription.valid_until, subscription.updated_at) - lease_seconds = lease_seconds |> to_string - - params = %{ - "hub.challenge": challenge, - "hub.lease_seconds": lease_seconds, - "hub.topic": subscription.topic, - "hub.mode": "subscribe" - } - - url = hd(String.split(subscription.callback, "?")) - query = URI.parse(subscription.callback).query || "" - params = Map.merge(params, URI.decode_query(query)) - - with {:ok, response} <- getter.(url, [], params: params), - ^challenge <- response.body do - changeset = Changeset.change(subscription, %{state: "active"}) - Repo.update(changeset) - else - e -> - Logger.debug("Couldn't verify subscription") - Logger.debug(inspect(e)) - {:error, subscription} - end - end - - @supported_activities [ - "Create", - "Follow", - "Like", - "Announce", - "Undo", - "Delete" - ] - - def is_representable?(%Activity{data: %{"type" => type}} = activity) - when type in @supported_activities, - do: Visibility.is_public?(activity) - - def is_representable?(_), do: false - - def publish(topic, user, %{data: %{"type" => type}} = activity) - when type in @supported_activities do - response = - user - |> FeedRepresenter.to_simple_form([activity], [user]) - |> :xmerl.export_simple(:xmerl_xml) - |> to_string - - query = - from( - sub in WebsubServerSubscription, - where: sub.topic == ^topic and sub.state == "active", - where: fragment("? > (NOW() at time zone 'UTC')", sub.valid_until) - ) - - subscriptions = Repo.all(query) - - callbacks = Enum.map(subscriptions, & &1.callback) - reachable_callbacks_metadata = Instances.filter_reachable(callbacks) - reachable_callbacks = Map.keys(reachable_callbacks_metadata) - - subscriptions - |> Enum.filter(&(&1.callback in reachable_callbacks)) - |> Enum.each(fn sub -> - data = %{ - xml: response, - topic: topic, - callback: sub.callback, - secret: sub.secret, - unreachable_since: reachable_callbacks_metadata[sub.callback] - } - - Publisher.enqueue_one(__MODULE__, data) - end) - end - - def publish(_, _, _), do: "" - - def publish(actor, activity), do: publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) - - def sign(secret, doc) do - :crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase() - end - - def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) do - with {:ok, topic} <- valid_topic(params, user), - {:ok, lease_time} <- lease_time(params), - secret <- params["hub.secret"], - callback <- params["hub.callback"] do - subscription = get_subscription(topic, callback) - - data = %{ - state: subscription.state || "requested", - topic: topic, - secret: secret, - callback: callback - } - - change = Changeset.change(subscription, data) - websub = Repo.insert_or_update!(change) - - change = - Changeset.change(websub, %{valid_until: NaiveDateTime.add(websub.updated_at, lease_time)}) - - websub = Repo.update!(change) - - Federator.verify_websub(websub) - - {:ok, websub} - else - {:error, reason} -> - Logger.debug("Couldn't create subscription") - Logger.debug(inspect(reason)) - - {:error, reason} - end - end - - def incoming_subscription_request(user, params) do - Logger.info("Unhandled WebSub request for #{user.nickname}: #{inspect(params)}") - - {:error, "Invalid WebSub request"} - end - - defp get_subscription(topic, callback) do - Repo.get_by(WebsubServerSubscription, topic: topic, callback: callback) || - %WebsubServerSubscription{} - end - - # Temp hack for mastodon. - defp lease_time(%{"hub.lease_seconds" => ""}) do - # three days - {:ok, 60 * 60 * 24 * 3} - end - - defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do - {:ok, String.to_integer(lease_seconds)} - end - - defp lease_time(_) do - # three days - {:ok, 60 * 60 * 24 * 3} - end - - defp valid_topic(%{"hub.topic" => topic}, user) do - if topic == OStatus.feed_path(user) do - {:ok, OStatus.feed_path(user)} - else - {:error, "Wrong topic requested, expected #{OStatus.feed_path(user)}, got #{topic}"} - end - end - - def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do - topic = subscribed.info.topic - # FIXME: Race condition, use transactions - {:ok, subscription} = - with subscription when not is_nil(subscription) <- - Repo.get_by(WebsubClientSubscription, topic: topic) do - subscribers = [subscriber.ap_id | subscription.subscribers] |> Enum.uniq() - change = Ecto.Changeset.change(subscription, %{subscribers: subscribers}) - Repo.update(change) - else - _e -> - subscription = %WebsubClientSubscription{ - topic: topic, - hub: subscribed.info.hub, - subscribers: [subscriber.ap_id], - state: "requested", - secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64(), - user: subscribed - } - - Repo.insert(subscription) - end - - requester.(subscription) - end - - def gather_feed_data(topic, getter \\ &HTTP.get/1) do - with {:ok, response} <- getter.(topic), - status when status in 200..299 <- response.status, - body <- response.body, - doc <- XML.parse_document(body), - uri when not is_nil(uri) <- XML.string_from_xpath("/feed/author[1]/uri", doc), - hub when not is_nil(hub) <- XML.string_from_xpath(~S{/feed/link[@rel="hub"]/@href}, doc) do - name = XML.string_from_xpath("/feed/author[1]/name", doc) - preferred_username = XML.string_from_xpath("/feed/author[1]/poco:preferredUsername", doc) - display_name = XML.string_from_xpath("/feed/author[1]/poco:displayName", doc) - avatar = OStatus.make_avatar_object(doc) - bio = XML.string_from_xpath("/feed/author[1]/summary", doc) - - {:ok, - %{ - "uri" => uri, - "hub" => hub, - "nickname" => preferred_username || name, - "name" => display_name || name, - "host" => URI.parse(uri).host, - "avatar" => avatar, - "bio" => bio - }} - else - e -> - {:error, e} - end - end - - def request_subscription(websub, poster \\ &HTTP.post/3, timeout \\ 10_000) do - data = [ - "hub.mode": "subscribe", - "hub.topic": websub.topic, - "hub.secret": websub.secret, - "hub.callback": Helpers.websub_url(Endpoint, :websub_subscription_confirmation, websub.id) - ] - - # This checks once a second if we are confirmed yet - websub_checker = fn -> - helper = fn helper -> - :timer.sleep(1000) - websub = Repo.get_by(WebsubClientSubscription, id: websub.id, state: "accepted") - if websub, do: websub, else: helper.(helper) - end - - helper.(helper) - end - - task = Task.async(websub_checker) - - with {:ok, %{status: 202}} <- - poster.(websub.hub, {:form, data}, "Content-type": "application/x-www-form-urlencoded"), - {:ok, websub} <- Task.yield(task, timeout) do - {:ok, websub} - else - e -> - Task.shutdown(task) - - change = Ecto.Changeset.change(websub, %{state: "rejected"}) - {:ok, websub} = Repo.update(change) - - Logger.debug(fn -> "Couldn't confirm subscription: #{inspect(websub)}" end) - Logger.debug(fn -> "error: #{inspect(e)}" end) - - {:error, websub} - end - end - - def refresh_subscriptions(delta \\ 60 * 60 * 24) do - Logger.debug("Refreshing subscriptions") - - cut_off = NaiveDateTime.add(NaiveDateTime.utc_now(), delta) - - query = from(sub in WebsubClientSubscription, where: sub.valid_until < ^cut_off) - - subs = Repo.all(query) - - Enum.each(subs, fn sub -> - Federator.request_subscription(sub) - end) - end - - def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret} = params) do - signature = sign(secret || "", xml) - Logger.info(fn -> "Pushing #{topic} to #{callback}" end) - - with {:ok, %{status: code}} when code in 200..299 <- - HTTP.post( - callback, - xml, - [ - {"Content-Type", "application/atom+xml"}, - {"X-Hub-Signature", "sha1=#{signature}"} - ] - ) do - if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], - do: Instances.set_reachable(callback) - - Logger.info(fn -> "Pushed to #{callback}, code #{code}" end) - {:ok, code} - else - {_post_result, response} -> - unless params[:unreachable_since], do: Instances.set_reachable(callback) - Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(response)}" end) - {:error, response} - end - end - - def gather_webfinger_links(%User{} = user) do - [ - %{ - "rel" => "http://schemas.google.com/g/2010#updates-from", - "type" => "application/atom+xml", - "href" => OStatus.feed_path(user) - }, - %{ - "rel" => "http://ostatus.org/schema/1.0/subscribe", - "template" => OStatus.remote_follow_path() - } - ] - end - - def gather_nodeinfo_protocol_names, do: ["ostatus"] -end diff --git a/lib/pleroma/web/websub/websub_client_subscription.ex b/lib/pleroma/web/websub/websub_client_subscription.ex deleted file mode 100644 index 23a04b87d..000000000 --- a/lib/pleroma/web/websub/websub_client_subscription.ex +++ /dev/null @@ -1,20 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Websub.WebsubClientSubscription do - use Ecto.Schema - alias Pleroma.User - - schema "websub_client_subscriptions" do - field(:topic, :string) - field(:secret, :string) - field(:valid_until, :naive_datetime_usec) - field(:state, :string) - field(:subscribers, {:array, :string}, default: []) - field(:hub, :string) - belongs_to(:user, User, type: FlakeId.Ecto.CompatType) - - timestamps() - end -end diff --git a/lib/pleroma/web/websub/websub_controller.ex b/lib/pleroma/web/websub/websub_controller.ex deleted file mode 100644 index 9e8b48b80..000000000 --- a/lib/pleroma/web/websub/websub_controller.ex +++ /dev/null @@ -1,99 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Websub.WebsubController do - use Pleroma.Web, :controller - - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.Federator - alias Pleroma.Web.Websub - alias Pleroma.Web.Websub.WebsubClientSubscription - - require Logger - - plug( - Pleroma.Web.FederatingPlug - when action in [ - :websub_subscription_request, - :websub_subscription_confirmation, - :websub_incoming - ] - ) - - def websub_subscription_request(conn, %{"nickname" => nickname} = params) do - user = User.get_cached_by_nickname(nickname) - - with {:ok, _websub} <- Websub.incoming_subscription_request(user, params) do - conn - |> send_resp(202, "Accepted") - else - {:error, reason} -> - conn - |> send_resp(500, reason) - end - end - - # TODO: Extract this into the Websub module - def websub_subscription_confirmation( - conn, - %{ - "id" => id, - "hub.mode" => "subscribe", - "hub.challenge" => challenge, - "hub.topic" => topic - } = params - ) do - Logger.debug("Got WebSub confirmation") - Logger.debug(inspect(params)) - - lease_seconds = - if params["hub.lease_seconds"] do - String.to_integer(params["hub.lease_seconds"]) - else - # Guess 3 days - 60 * 60 * 24 * 3 - end - - with %WebsubClientSubscription{} = websub <- - Repo.get_by(WebsubClientSubscription, id: id, topic: topic) do - valid_until = NaiveDateTime.add(NaiveDateTime.utc_now(), lease_seconds) - change = Ecto.Changeset.change(websub, %{state: "accepted", valid_until: valid_until}) - {:ok, _websub} = Repo.update(change) - - conn - |> send_resp(200, challenge) - else - _e -> - conn - |> send_resp(500, "Error") - end - end - - def websub_subscription_confirmation(conn, params) do - Logger.info("Invalid WebSub confirmation request: #{inspect(params)}") - - conn - |> send_resp(500, "Invalid parameters") - end - - def websub_incoming(conn, %{"id" => id}) do - with "sha1=" <> signature <- hd(get_req_header(conn, "x-hub-signature")), - signature <- String.downcase(signature), - %WebsubClientSubscription{} = websub <- Repo.get(WebsubClientSubscription, id), - {:ok, body, _conn} = read_body(conn), - ^signature <- Websub.sign(websub.secret, body) do - Federator.incoming_doc(body) - - conn - |> send_resp(200, "OK") - else - _e -> - Logger.debug("Can't handle incoming subscription post") - - conn - |> send_resp(500, "Error") - end - end -end diff --git a/lib/pleroma/web/websub/websub_server_subscription.ex b/lib/pleroma/web/websub/websub_server_subscription.ex deleted file mode 100644 index d0ef548da..000000000 --- a/lib/pleroma/web/websub/websub_server_subscription.ex +++ /dev/null @@ -1,17 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Websub.WebsubServerSubscription do - use Ecto.Schema - - schema "websub_server_subscriptions" do - field(:topic, :string) - field(:callback, :string) - field(:secret, :string) - field(:valid_until, :naive_datetime) - field(:state, :string) - - timestamps() - end -end diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index 83d528a66..8ad756b62 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -8,10 +8,6 @@ defmodule Pleroma.Workers.ReceiverWorker do use Pleroma.Workers.WorkerHelper, queue: "federator_incoming" @impl Oban.Worker - def perform(%{"op" => "incoming_doc", "body" => doc}, _job) do - Federator.perform(:incoming_doc, doc) - end - def perform(%{"op" => "incoming_ap_doc", "params" => params}, _job) do Federator.perform(:incoming_ap_doc, params) end diff --git a/lib/pleroma/workers/subscriber_worker.ex b/lib/pleroma/workers/subscriber_worker.ex deleted file mode 100644 index fc490e300..000000000 --- a/lib/pleroma/workers/subscriber_worker.ex +++ /dev/null @@ -1,26 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.SubscriberWorker do - alias Pleroma.Repo - alias Pleroma.Web.Federator - alias Pleroma.Web.Websub - - use Pleroma.Workers.WorkerHelper, queue: "federator_outgoing" - - @impl Oban.Worker - def perform(%{"op" => "refresh_subscriptions"}, _job) do - Federator.perform(:refresh_subscriptions) - end - - def perform(%{"op" => "request_subscription", "websub_id" => websub_id}, _job) do - websub = Repo.get(Websub.WebsubClientSubscription, websub_id) - Federator.perform(:request_subscription, websub) - end - - def perform(%{"op" => "verify_websub", "websub_id" => websub_id}, _job) do - websub = Repo.get(Websub.WebsubServerSubscription, websub_id) - Federator.perform(:verify_websub, websub) - end -end diff --git a/mix.exs b/mix.exs index 3a605b455..dd7c7e979 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do def project do [ app: :pleroma, - version: version("1.0.0"), + version: version("1.1.50"), elixir: "~> 1.8", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), @@ -63,12 +63,13 @@ def copy_nginx_config(%{path: target_path} = release) do def application do [ mod: {Pleroma.Application, []}, - extra_applications: [:logger, :runtime_tools, :comeonin, :quack], + extra_applications: [:logger, :runtime_tools, :comeonin, :quack, :fast_sanitize, :swarm], included_applications: [:ex_syslogger] ] end # Specifies which paths to compile per environment. + defp elixirc_paths(:benchmark), do: ["lib", "benchmarks"] defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] @@ -107,8 +108,8 @@ defp deps do {:comeonin, "~> 4.1.1"}, {:pbkdf2_elixir, "~> 0.12.3"}, {:trailing_format_plug, "~> 0.0.7"}, - {:html_sanitize_ex, "~> 1.3.0"}, - {:html_entities, "~> 0.4"}, + {:fast_sanitize, "~> 0.1"}, + {:html_entities, "~> 0.5", override: true}, {:phoenix_html, "~> 2.10"}, {:calendar, "~> 0.17.4"}, {:cachex, "~> 3.0.2"}, @@ -220,7 +221,10 @@ defp version(version) do with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]), branch_name <- String.trim(branch_name), branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name, - true <- branch_name not in ["master", "HEAD"] do + true <- + !Enum.any?(["master", "HEAD", "release/", "stable"], fn name -> + String.starts_with?(name, branch_name) + end) do branch_name = branch_name |> String.trim() diff --git a/mix.lock b/mix.lock index 5f740638c..c707667b2 100644 --- a/mix.lock +++ b/mix.lock @@ -13,44 +13,46 @@ "comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, - "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"}, + "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, + "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm"}, "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "crontab": {:hex, :crontab, "1.1.7", "b9219f0bdc8678b94143655a8f229716c5810c0636a4489f98c0956137e53985", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, + "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm"}, "db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"}, - "earmark": {:hex, :earmark, "1.3.6", "ce1d0675e10a5bb46b007549362bd3f5f08908843957687d8484fe7f37466b19", [:mix], [], "hexpm"}, - "ecto": {:hex, :ecto, "3.2.0", "940e2598813f205223d60c78d66e514afe1db5167ed8075510a59e496619cfb5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "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"}, "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"}, - "eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"}, + "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, - "ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_aws": {:hex, :ex_aws, "2.1.1", "1e4de2106cfbf4e837de41be41cd15813eabc722315e388f0d6bb3732cec47cd", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.2", "c0258bbdfea55de4f98f0b2f0ca61fe402cc696f573815134beb1866e778f47b", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, "ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"}, "ex_rated": {:hex, :ex_rated, "1.3.3", "30ecbdabe91f7eaa9d37fa4e81c85ba420f371babeb9d1910adbcd79ec798d27", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"}, "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]}, - "excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.11.2", "0c6f2c8db7683b0caa9d490fb8125709c54580b4255ffa7ad35f3264b075a643", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, + "fast_html": {:hex, :fast_html, "0.99.0", "ea740358b15c7da6085b421b775f22d4f2c6928a28a15ebb5ad4e8a2ce00350b", [:make, :mix], [], "hexpm"}, + "fast_sanitize": {:hex, :fast_sanitize, "0.1.1", "a403c3c09369e23423d3e6beb14068ad07be82741d10b293c71abac445dcc636", [: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"}, - "gen_smtp": {:hex, :gen_smtp, "0.14.0", "39846a03522456077c6429b4badfd1d55e5e7d0fdfb65e935b7c5e38549d9202", [:rebar3], [], "hexpm"}, - "gen_stage": {:hex, :gen_stage, "0.14.2", "6a2a578a510c5bfca8a45e6b27552f613b41cf584b58210f017088d3d17d0b14", [:mix], [], "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"}, - "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"}, + "gettext": {:hex, :gettext, "0.17.1", "8baab33482df4907b3eae22f719da492cee3981a26e649b9c2be1c0192616962", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, - "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"}, + "html_entities": {:hex, :html_entities, "0.5.0", "40f5c5b9cbe23073b48a4e69c67b6c11974f623a76165e2b92d098c0e88ccb1d", [:mix], [], "hexpm"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]}, - "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "httpoison": {:hex, :httpoison, "1.6.1", "2ce5bf6e535cd0ab02e905ba8c276580bab80052c5c549f53ddea52d72e81f33", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "joken": {:hex, :joken, "2.0.1", "ec9ab31bf660f343380da033b3316855197c8d4c6ef597fa3fcb451b326beb14", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"}, + "joken": {:hex, :joken, "2.1.0", "bf21a73105d82649f617c5e59a7f8919aa47013d2519ebcc39d998d8d12adda9", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"}, "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, @@ -63,16 +65,18 @@ "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"}, "mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm"}, + "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"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, - "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"}, - "phoenix": {:hex, :phoenix, "1.4.9", "746d098e10741c334d88143d3c94cab1756435f94387a63441792e66ec0ee974", [: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"}, + "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"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_html": {:hex, :phoenix_html, "2.13.1", "fa8f034b5328e2dfa0e4131b5569379003f34bc1fafdaa84985b0b9d2f12e68b", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "0.2.0", "a7e0b32077cd6d2323ae15198839b05d9caddfa20663fd85787479e81f89520e", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 0.1", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm"}, - "plug": {:hex, :plug, "1.8.2", "0bcce1daa420f189a6491f3940cc77ea7fb1919761175c9c3b59800d897440fc", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, + "plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, "plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, @@ -80,7 +84,7 @@ "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.15.1", "23ce3417de70f4c0e9e7419ad85bdabcc6860a6925fe2c6f3b1b5b1e8e47bf2f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, "prometheus": {:hex, :prometheus, "4.4.1", "1e96073b3ed7788053768fea779cbc896ddc3bdd9ba60687f2ad50b252ac87d6", [:mix, :rebar3], [], "hexpm"}, - "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.1", "6c768ea9654de871e5b32fab2eac348467b3021604ebebbcbd8bcbe806a65ed5", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"}, + "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"}, "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"}, "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.3.0", "c4b527e0b3a9ef1af26bdcfbfad3998f37795b9185d475ca610fe4388fdd3bb5", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm"}, @@ -92,16 +96,16 @@ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"}, - "swoosh": {:hex, :swoosh, "0.23.2", "7dda95ff0bf54a2298328d6899c74dae1223777b43563ccebebb4b5d2b61df38", [: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"}, + "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"}, "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"}, - "tzdata": {:hex, :tzdata, "0.5.21", "8cbf3607fcce69636c672d5be2bbb08687fe26639a62bdcc283d267277db7cf0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "tzdata": {:hex, :tzdata, "0.5.22", "f2ba9105117ee0360eae2eca389783ef7db36d533899b2e84559404dbc77ebb8", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "ueberauth": {:hex, :ueberauth, "0.6.2", "25a31111249d60bad8b65438b2306a4dc91f3208faa62f5a8c33e8713989b2e8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"}, - "web_push_encryption": {:hex, :web_push_encryption, "0.2.1", "d42cecf73420d9dc0053ba3299cc8c8d6ff2be2487d67ca2a57265868e4d9a98", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "web_push_encryption": {:hex, :web_push_encryption, "0.2.3", "a0ceab85a805a30852f143d22d71c434046fbdbafbc7292e7887cec500826a80", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []}, } diff --git a/priv/repo/migrations/20190414125034_migrate_old_bookmarks.exs b/priv/repo/migrations/20190414125034_migrate_old_bookmarks.exs index f3928a149..99102117f 100644 --- a/priv/repo/migrations/20190414125034_migrate_old_bookmarks.exs +++ b/priv/repo/migrations/20190414125034_migrate_old_bookmarks.exs @@ -8,10 +8,10 @@ defmodule Pleroma.Repo.Migrations.MigrateOldBookmarks do def up do query = - from(u in User, + from(u in "users", where: u.local == true, - where: fragment("array_length(bookmarks, 1)") > 0, - select: %{id: u.id, bookmarks: fragment("bookmarks")} + where: fragment("array_length(?, 1)", u.bookmarks) > 0, + select: %{id: u.id, bookmarks: u.bookmarks} ) Repo.stream(query) diff --git a/priv/repo/migrations/20190710125158_add_following_address_from_source_data.exs b/priv/repo/migrations/20190710125158_add_following_address_from_source_data.exs index 779aa382e..a5170d521 100644 --- a/priv/repo/migrations/20190710125158_add_following_address_from_source_data.exs +++ b/priv/repo/migrations/20190710125158_add_following_address_from_source_data.exs @@ -5,7 +5,11 @@ defmodule Pleroma.Repo.Migrations.AddFollowingAddressFromSourceData do def change do query = - User.external_users_query() + User.Query.build(%{ + external: true, + legacy_active: true, + order_by: :id + }) |> select([u], struct(u, [:id, :ap_id, :info])) Pleroma.Repo.stream(query) diff --git a/priv/repo/migrations/20190711042021_create_safe_jsonb_set.exs b/priv/repo/migrations/20190711042021_create_safe_jsonb_set.exs new file mode 100644 index 000000000..2f336a5e8 --- /dev/null +++ b/priv/repo/migrations/20190711042021_create_safe_jsonb_set.exs @@ -0,0 +1,22 @@ +defmodule Pleroma.Repo.Migrations.CreateSafeJsonbSet do + use Ecto.Migration + alias Pleroma.User + + def change do + execute(""" + create or replace function safe_jsonb_set(target jsonb, path text[], new_value jsonb, create_missing boolean default true) returns jsonb as $$ + declare + result jsonb; + begin + result := jsonb_set(target, path, coalesce(new_value, 'null'::jsonb), create_missing); + if result is NULL then + raise 'jsonb_set tried to wipe the object, please report this incindent to Pleroma bug tracker. https://git.pleroma.social/pleroma/pleroma/issues/new'; + return target; + else + return result; + end if; + end; + $$ language plpgsql; + """) + end +end diff --git a/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs b/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs index bc4e828cc..fc9bf70ba 100644 --- a/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs +++ b/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs @@ -1,10 +1,9 @@ defmodule Pleroma.Repo.Migrations.CopyMutedToMutedNotifications do use Ecto.Migration - alias Pleroma.User def change do execute( - "update users set info = jsonb_set(info, '{muted_notifications}', info->'mutes', true) where local = true" + "update users set info = safe_jsonb_set(info, '{muted_notifications}', info->'mutes', true) where local = true" ) end end diff --git a/priv/repo/migrations/20191007073319_create_following_relationships.exs b/priv/repo/migrations/20191007073319_create_following_relationships.exs new file mode 100644 index 000000000..d49e24ee4 --- /dev/null +++ b/priv/repo/migrations/20191007073319_create_following_relationships.exs @@ -0,0 +1,149 @@ +defmodule Pleroma.Repo.Migrations.CreateFollowingRelationships do + use Ecto.Migration + + def change do + create_if_not_exists table(:following_relationships) do + add(:follower_id, references(:users, type: :uuid, on_delete: :delete_all), null: false) + add(:following_id, references(:users, type: :uuid, on_delete: :delete_all), null: false) + add(:state, :string, null: false) + + timestamps() + end + + create_if_not_exists(index(:following_relationships, :follower_id)) + create_if_not_exists(unique_index(:following_relationships, [:follower_id, :following_id])) + + execute(update_thread_visibility(), restore_thread_visibility()) + end + + # The only difference between the original version: `actor_user` replaced with `actor_user_following` + def update_thread_visibility do + """ + CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar) RETURNS boolean AS $$ + DECLARE + public varchar := 'https://www.w3.org/ns/activitystreams#Public'; + child objects%ROWTYPE; + activity activities%ROWTYPE; + author_fa varchar; + valid_recipients varchar[]; + actor_user_following varchar[]; + BEGIN + --- Fetch actor following + SELECT array_agg(following.follower_address) INTO actor_user_following FROM following_relationships + JOIN users ON users.id = following_relationships.follower_id + JOIN users AS following ON following.id = following_relationships.following_id + WHERE users.ap_id = actor; + + --- Fetch our initial activity. + SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id; + + LOOP + --- Ensure that we have an activity before continuing. + --- If we don't, the thread is not satisfiable. + IF activity IS NULL THEN + RETURN false; + END IF; + + --- We only care about Create activities. + IF activity.data->>'type' != 'Create' THEN + RETURN true; + END IF; + + --- Normalize the child object into child. + SELECT * INTO child FROM objects + INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' + WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id'; + + --- Fetch the author's AS2 following collection. + SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor; + + --- Prepare valid recipients array. + valid_recipients := ARRAY[actor, public]; + IF ARRAY[author_fa] && actor_user_following THEN + valid_recipients := valid_recipients || author_fa; + END IF; + + --- Check visibility. + IF NOT valid_recipients && activity.recipients THEN + --- activity not visible, break out of the loop + RETURN false; + END IF; + + --- If there's a parent, load it and do this all over again. + IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN + SELECT * INTO activity FROM activities + INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' + WHERE child.data->>'inReplyTo' = objects.data->>'id'; + ELSE + RETURN true; + END IF; + END LOOP; + END; + $$ LANGUAGE plpgsql IMMUTABLE; + """ + end + + # priv/repo/migrations/20190515222404_add_thread_visibility_function.exs + def restore_thread_visibility do + """ + CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar) RETURNS boolean AS $$ + DECLARE + public varchar := 'https://www.w3.org/ns/activitystreams#Public'; + child objects%ROWTYPE; + activity activities%ROWTYPE; + actor_user users%ROWTYPE; + author_fa varchar; + valid_recipients varchar[]; + BEGIN + --- Fetch our actor. + SELECT * INTO actor_user FROM users WHERE users.ap_id = actor; + + --- Fetch our initial activity. + SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id; + + LOOP + --- Ensure that we have an activity before continuing. + --- If we don't, the thread is not satisfiable. + IF activity IS NULL THEN + RETURN false; + END IF; + + --- We only care about Create activities. + IF activity.data->>'type' != 'Create' THEN + RETURN true; + END IF; + + --- Normalize the child object into child. + SELECT * INTO child FROM objects + INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' + WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id'; + + --- Fetch the author's AS2 following collection. + SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor; + + --- Prepare valid recipients array. + valid_recipients := ARRAY[actor, public]; + IF ARRAY[author_fa] && actor_user.following THEN + valid_recipients := valid_recipients || author_fa; + END IF; + + --- Check visibility. + IF NOT valid_recipients && activity.recipients THEN + --- activity not visible, break out of the loop + RETURN false; + END IF; + + --- If there's a parent, load it and do this all over again. + IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN + SELECT * INTO activity FROM activities + INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id' + WHERE child.data->>'inReplyTo' = objects.data->>'id'; + ELSE + RETURN true; + END IF; + END LOOP; + END; + $$ LANGUAGE plpgsql IMMUTABLE; + """ + end +end diff --git a/priv/repo/migrations/20191008132217_migrate_following_relationships.exs b/priv/repo/migrations/20191008132217_migrate_following_relationships.exs new file mode 100644 index 000000000..9d5c2648f --- /dev/null +++ b/priv/repo/migrations/20191008132217_migrate_following_relationships.exs @@ -0,0 +1,89 @@ +defmodule Pleroma.Repo.Migrations.MigrateFollowingRelationships do + use Ecto.Migration + + def change do + execute(import_following_from_users(), "") + execute(import_following_from_activities(), restore_following_column()) + end + + defp import_following_from_users do + """ + INSERT INTO following_relationships (follower_id, following_id, state, inserted_at, updated_at) + SELECT + relations.follower_id, + following.id, + 'accept', + now(), + now() + FROM ( + SELECT + users.id AS follower_id, + unnest(users.following) AS following_ap_id + FROM + users + WHERE + users.following != '{}' + AND users.local = false OR users.local = true AND users.email IS NOT NULL -- Exclude `internal/fetch` and `relay` + ) AS relations + JOIN users AS "following" ON "following".follower_address = relations.following_ap_id + + WHERE relations.follower_id != following.id + ON CONFLICT DO NOTHING + """ + end + + defp import_following_from_activities do + """ + INSERT INTO + following_relationships ( + follower_id, + following_id, + state, + inserted_at, + updated_at + ) + SELECT + followers.id, + following.id, + activities.data ->> 'state', + (activities.data ->> 'published') :: timestamp, + now() + FROM + activities + JOIN users AS followers ON (activities.actor = followers.ap_id) + JOIN users AS following ON (activities.data ->> 'object' = following.ap_id) + WHERE + activities.data ->> 'type' = 'Follow' + AND activities.data ->> 'state' IN ('accept', 'pending', 'reject') + ORDER BY activities.updated_at DESC + ON CONFLICT DO NOTHING + """ + end + + defp restore_following_column do + """ + UPDATE + users + SET + following = following_query.following_array, + updated_at = now() + FROM ( + SELECT + follower.id AS follower_id, + CASE follower.local + WHEN TRUE THEN + array_prepend(follower.follower_address, array_agg(following.follower_address)) + ELSE + array_agg(following.follower_address) + END AS following_array + FROM + following_relationships + JOIN users AS follower ON follower.id = following_relationships.follower_id + JOIN users AS following ON following.id = following_relationships.following_id + GROUP BY + follower.id) AS following_query + WHERE + following_query.follower_id = users.id + """ + end +end diff --git a/priv/repo/migrations/20191008132427_drop_users_following.exs b/priv/repo/migrations/20191008132427_drop_users_following.exs new file mode 100644 index 000000000..21c0af9f4 --- /dev/null +++ b/priv/repo/migrations/20191008132427_drop_users_following.exs @@ -0,0 +1,16 @@ +defmodule Pleroma.Repo.Migrations.DropUsersFollowing do + use Ecto.Migration + + # had to disable these to be able to restore `following` index concurrently + # https://hexdocs.pm/ecto_sql/Ecto.Migration.html#index/3-adding-dropping-indexes-concurrently + @disable_ddl_transaction true + @disable_migration_lock true + + def change do + drop(index(:users, [:following], concurrently: true, using: :gin)) + + alter table(:users) do + remove(:following, {:array, :string}, default: []) + end + end +end diff --git a/priv/repo/migrations/20191009154606_add_user_info_columns.exs b/priv/repo/migrations/20191009154606_add_user_info_columns.exs new file mode 100644 index 000000000..22a5a377f --- /dev/null +++ b/priv/repo/migrations/20191009154606_add_user_info_columns.exs @@ -0,0 +1,53 @@ +defmodule Pleroma.Repo.Migrations.AddUsersInfoColumns do + use Ecto.Migration + + @jsonb_array_default "'[]'::jsonb" + + def change do + alter table(:users) do + add_if_not_exists(:banner, :map, default: %{}) + add_if_not_exists(:background, :map, default: %{}) + add_if_not_exists(:source_data, :map, default: %{}) + add_if_not_exists(:note_count, :integer, default: 0) + add_if_not_exists(:follower_count, :integer, default: 0) + add_if_not_exists(:following_count, :integer, default: nil) + add_if_not_exists(:locked, :boolean, default: false, null: false) + add_if_not_exists(:confirmation_pending, :boolean, default: false, null: false) + add_if_not_exists(:password_reset_pending, :boolean, default: false, null: false) + add_if_not_exists(:confirmation_token, :text, default: nil) + add_if_not_exists(:default_scope, :string, default: "public") + add_if_not_exists(:blocks, {:array, :text}, default: []) + add_if_not_exists(:domain_blocks, {:array, :text}, default: []) + add_if_not_exists(:mutes, {:array, :text}, default: []) + add_if_not_exists(:muted_reblogs, {:array, :text}, default: []) + add_if_not_exists(:muted_notifications, {:array, :text}, default: []) + add_if_not_exists(:subscribers, {:array, :text}, default: []) + add_if_not_exists(:deactivated, :boolean, default: false, null: false) + add_if_not_exists(:no_rich_text, :boolean, default: false, null: false) + add_if_not_exists(:ap_enabled, :boolean, default: false, null: false) + add_if_not_exists(:is_moderator, :boolean, default: false, null: false) + add_if_not_exists(:is_admin, :boolean, default: false, null: false) + add_if_not_exists(:show_role, :boolean, default: true, null: false) + add_if_not_exists(:settings, :map, default: nil) + add_if_not_exists(:magic_key, :text, default: nil) + add_if_not_exists(:uri, :text, default: nil) + add_if_not_exists(:hide_followers_count, :boolean, default: false, null: false) + add_if_not_exists(:hide_follows_count, :boolean, default: false, null: false) + add_if_not_exists(:hide_followers, :boolean, default: false, null: false) + add_if_not_exists(:hide_follows, :boolean, default: false, null: false) + add_if_not_exists(:hide_favorites, :boolean, default: true, null: false) + add_if_not_exists(:unread_conversation_count, :integer, default: 0) + add_if_not_exists(:pinned_activities, {:array, :text}, default: []) + add_if_not_exists(:email_notifications, :map, default: %{"digest" => false}) + add_if_not_exists(:mascot, :map, default: nil) + add_if_not_exists(:emoji, :map, default: fragment(@jsonb_array_default)) + add_if_not_exists(:pleroma_settings_store, :map, default: %{}) + add_if_not_exists(:fields, :map, default: fragment(@jsonb_array_default)) + add_if_not_exists(:raw_fields, :map, default: fragment(@jsonb_array_default)) + add_if_not_exists(:discoverable, :boolean, default: false, null: false) + add_if_not_exists(:invisible, :boolean, default: false, null: false) + add_if_not_exists(:notification_settings, :map, default: %{}) + add_if_not_exists(:skip_thread_containment, :boolean, default: false, null: false) + end + end +end diff --git a/priv/repo/migrations/20191009154608_copy_users_info_fields_to_users.exs b/priv/repo/migrations/20191009154608_copy_users_info_fields_to_users.exs new file mode 100644 index 000000000..cc5ae6920 --- /dev/null +++ b/priv/repo/migrations/20191009154608_copy_users_info_fields_to_users.exs @@ -0,0 +1,145 @@ +defmodule Pleroma.Repo.Migrations.CopyUsersInfoFieldsToUsers do + use Ecto.Migration + + @jsonb_array_default "'[]'::jsonb" + + @info_fields [ + :banner, + :background, + :source_data, + :note_count, + :follower_count, + :following_count, + :locked, + :confirmation_pending, + :password_reset_pending, + :confirmation_token, + :default_scope, + :blocks, + :domain_blocks, + :mutes, + :muted_reblogs, + :muted_notifications, + :subscribers, + :deactivated, + :no_rich_text, + :ap_enabled, + :is_moderator, + :is_admin, + :show_role, + :settings, + :magic_key, + :uri, + :hide_followers_count, + :hide_follows_count, + :hide_followers, + :hide_follows, + :hide_favorites, + :unread_conversation_count, + :pinned_activities, + :email_notifications, + :mascot, + :emoji, + :pleroma_settings_store, + :fields, + :raw_fields, + :discoverable, + :invisible, + :skip_thread_containment, + :notification_settings + ] + + @jsonb_fields [ + :banner, + :background, + :source_data, + :settings, + :email_notifications, + :mascot, + :pleroma_settings_store, + :notification_settings + ] + + @array_jsonb_fields [:emoji, :fields, :raw_fields] + + @int_fields [:note_count, :follower_count, :following_count, :unread_conversation_count] + + @boolean_fields [ + :locked, + :confirmation_pending, + :password_reset_pending, + :deactivated, + :no_rich_text, + :ap_enabled, + :is_moderator, + :is_admin, + :show_role, + :hide_followers_count, + :hide_follows_count, + :hide_followers, + :hide_follows, + :hide_favorites, + :discoverable, + :invisible, + :skip_thread_containment + ] + + @array_text_fields [ + :blocks, + :domain_blocks, + :mutes, + :muted_reblogs, + :muted_notifications, + :subscribers, + :pinned_activities + ] + + def change do + if direction() == :up do + sets = + for f <- @info_fields do + set_field = "#{f} =" + + # Coercion of null::jsonb to NULL + jsonb = "case when info->>'#{f}' IS NULL then null else info->'#{f}' end" + + cond do + f in @jsonb_fields -> + "#{set_field} #{jsonb}" + + f in @array_jsonb_fields -> + "#{set_field} coalesce(#{jsonb}, #{@jsonb_array_default})" + + f in @int_fields -> + "#{set_field} (info->>'#{f}')::int" + + f in @boolean_fields -> + "#{set_field} coalesce((info->>'#{f}')::boolean, false)" + + f in @array_text_fields -> + "#{set_field} ARRAY(SELECT jsonb_array_elements_text(#{jsonb}))" + + true -> + "#{set_field} info->>'#{f}'" + end + end + |> Enum.join(", ") + + execute("update users set " <> sets) + + for index_name <- [ + :users_deactivated_index, + :users_is_moderator_index, + :users_is_admin_index, + :users_subscribers_index + ] do + drop_if_exists(index(:users, [], name: index_name)) + end + end + + create_if_not_exists(index(:users, [:deactivated])) + create_if_not_exists(index(:users, [:is_moderator])) + create_if_not_exists(index(:users, [:is_admin])) + create_if_not_exists(index(:users, [:subscribers])) + end +end diff --git a/priv/repo/migrations/20191014181019_create_markers.exs b/priv/repo/migrations/20191014181019_create_markers.exs new file mode 100644 index 000000000..c717831ba --- /dev/null +++ b/priv/repo/migrations/20191014181019_create_markers.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.CreateMarkers do + use Ecto.Migration + + def change do + create_if_not_exists table(:markers) do + add(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:timeline, :string, default: "", null: false) + add(:last_read_id, :string, default: "", null: false) + add(:lock_version, :integer, default: 0, null: false) + timestamps() + end + + create_if_not_exists(unique_index(:markers, [:user_id, :timeline])) + end +end diff --git a/priv/repo/migrations/20191017225002_drop_websub_tables.exs b/priv/repo/migrations/20191017225002_drop_websub_tables.exs new file mode 100644 index 000000000..4cf67a59c --- /dev/null +++ b/priv/repo/migrations/20191017225002_drop_websub_tables.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.DropWebsubTables do + use Ecto.Migration + + def up do + drop_if_exists(table(:websub_client_subscriptions)) + drop_if_exists(table(:websub_server_subscriptions)) + end + + def down, do: :noop +end diff --git a/priv/repo/migrations/20191025143434_add_defaults_to_tables.exs b/priv/repo/migrations/20191025143434_add_defaults_to_tables.exs new file mode 100644 index 000000000..a5bc82335 --- /dev/null +++ b/priv/repo/migrations/20191025143434_add_defaults_to_tables.exs @@ -0,0 +1,68 @@ +defmodule Pleroma.Repo.Migrations.AddDefaultsToTables do + use Ecto.Migration + + def up do + execute("ALTER TABLE activities + ALTER COLUMN recipients SET DEFAULT ARRAY[]::character varying[]") + + execute("ALTER TABLE filters + ALTER COLUMN whole_word SET DEFAULT true") + + execute("ALTER TABLE push_subscriptions + ALTER COLUMN data SET DEFAULT '{}'::jsonb") + + execute(~s(ALTER TABLE users + ALTER COLUMN tags SET DEFAULT ARRAY[]::character varying[], + ALTER COLUMN notification_settings SET DEFAULT + '{"followers": true, "follows": true, "non_follows": true, "non_followers": true}'::jsonb)) + + # irreversible updates + + execute( + "UPDATE activities SET recipients = ARRAY[]::character varying[] WHERE recipients IS NULL" + ) + + execute("UPDATE filters SET whole_word = true WHERE whole_word IS NULL") + + execute("UPDATE push_subscriptions SET data = '{}'::jsonb WHERE data IS NULL") + + execute("UPDATE users SET source_data = '{}'::jsonb where source_data IS NULL") + execute("UPDATE users SET note_count = 0 where note_count IS NULL") + execute("UPDATE users SET background = '{}'::jsonb where background IS NULL") + execute("UPDATE users SET follower_count = 0 where follower_count IS NULL") + + execute( + "UPDATE users SET unread_conversation_count = 0 where unread_conversation_count IS NULL" + ) + + execute( + ~s(UPDATE users SET email_notifications = '{"digest": false}'::jsonb where email_notifications IS NULL) + ) + + execute("UPDATE users SET default_scope = 'public' where default_scope IS NULL") + + execute( + "UPDATE users SET pleroma_settings_store = '{}'::jsonb where pleroma_settings_store IS NULL" + ) + + execute("UPDATE users SET tags = ARRAY[]::character varying[] WHERE tags IS NULL") + execute(~s(UPDATE users SET notification_settings = + '{"followers": true, "follows": true, "non_follows": true, "non_followers": true}'::jsonb + WHERE notification_settings = '{}'::jsonb)) + end + + def down do + execute("ALTER TABLE activities + ALTER COLUMN recipients DROP DEFAULT") + + execute("ALTER TABLE filters + ALTER COLUMN whole_word DROP DEFAULT") + + execute("ALTER TABLE push_subscriptions + ALTER COLUMN data DROP DEFAULT") + + execute("ALTER TABLE users + ALTER COLUMN tags DROP DEFAULT, + ALTER COLUMN notification_settings SET DEFAULT '{}'::jsonb") + end +end diff --git a/priv/repo/migrations/20191026190317_set_not_null_for_activities.exs b/priv/repo/migrations/20191026190317_set_not_null_for_activities.exs new file mode 100644 index 000000000..9b66f3cff --- /dev/null +++ b/priv/repo/migrations/20191026190317_set_not_null_for_activities.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForActivities do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE activities + ALTER COLUMN data SET NOT NULL, + ALTER COLUMN local SET NOT NULL") + end + + def down do + execute("ALTER TABLE activities + ALTER COLUMN data DROP NOT NULL, + ALTER COLUMN local DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026190415_set_not_null_for_activity_expirations.exs b/priv/repo/migrations/20191026190415_set_not_null_for_activity_expirations.exs new file mode 100644 index 000000000..e41c69e4c --- /dev/null +++ b/priv/repo/migrations/20191026190415_set_not_null_for_activity_expirations.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForActivityExpirations do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE activity_expirations + ALTER COLUMN activity_id SET NOT NULL") + end + + def down do + execute("ALTER TABLE activity_expirations + ALTER COLUMN activity_id DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026190500_set_not_null_for_apps.exs b/priv/repo/migrations/20191026190500_set_not_null_for_apps.exs new file mode 100644 index 000000000..a6a44ddfe --- /dev/null +++ b/priv/repo/migrations/20191026190500_set_not_null_for_apps.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForApps do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE apps + ALTER COLUMN client_name SET NOT NULL, + ALTER COLUMN redirect_uris SET NOT NULL") + end + + def down do + execute("ALTER TABLE apps + ALTER COLUMN client_name DROP NOT NULL, + ALTER COLUMN redirect_uris DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026190533_set_not_null_for_bookmarks.exs b/priv/repo/migrations/20191026190533_set_not_null_for_bookmarks.exs new file mode 100644 index 000000000..5f3224003 --- /dev/null +++ b/priv/repo/migrations/20191026190533_set_not_null_for_bookmarks.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForBookmarks do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE bookmarks + ALTER COLUMN user_id SET NOT NULL, + ALTER COLUMN activity_id SET NOT NULL") + end + + def down do + execute("ALTER TABLE bookmarks + ALTER COLUMN user_id DROP NOT NULL, + ALTER COLUMN activity_id DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026190622_set_not_null_for_config.exs b/priv/repo/migrations/20191026190622_set_not_null_for_config.exs new file mode 100644 index 000000000..204272442 --- /dev/null +++ b/priv/repo/migrations/20191026190622_set_not_null_for_config.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForConfig do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE config + ALTER COLUMN key SET NOT NULL, + ALTER COLUMN value SET NOT NULL") + end + + def down do + execute("ALTER TABLE config + ALTER COLUMN key DROP NOT NULL, + ALTER COLUMN value DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026190712_set_not_null_for_conversation_participation_recipient_ships.exs b/priv/repo/migrations/20191026190712_set_not_null_for_conversation_participation_recipient_ships.exs new file mode 100644 index 000000000..a5ab1d3c9 --- /dev/null +++ b/priv/repo/migrations/20191026190712_set_not_null_for_conversation_participation_recipient_ships.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForConversationParticipationRecipientShips do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE conversation_participation_recipient_ships + ALTER COLUMN user_id SET NOT NULL, + ALTER COLUMN participation_id SET NOT NULL") + end + + def down do + execute("ALTER TABLE conversation_participation_recipient_ships + ALTER COLUMN user_id DROP NOT NULL, + ALTER COLUMN participation_id DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026190759_set_not_null_for_conversation_participations.exs b/priv/repo/migrations/20191026190759_set_not_null_for_conversation_participations.exs new file mode 100644 index 000000000..cabb1f29f --- /dev/null +++ b/priv/repo/migrations/20191026190759_set_not_null_for_conversation_participations.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForConversationParticipations do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE conversation_participations + ALTER COLUMN user_id SET NOT NULL, + ALTER COLUMN conversation_id SET NOT NULL, + ALTER COLUMN read SET NOT NULL") + end + + def down do + execute("ALTER TABLE conversation_participations + ALTER COLUMN user_id DROP NOT NULL, + ALTER COLUMN conversation_id DROP NOT NULL, + ALTER COLUMN read DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026190841_set_not_null_for_filters.exs b/priv/repo/migrations/20191026190841_set_not_null_for_filters.exs new file mode 100644 index 000000000..52d7e687a --- /dev/null +++ b/priv/repo/migrations/20191026190841_set_not_null_for_filters.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForFilters do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE filters + ALTER COLUMN user_id SET NOT NULL, + ALTER COLUMN filter_id SET NOT NULL, + ALTER COLUMN whole_word SET NOT NULL") + end + + def down do + execute("ALTER TABLE filters + ALTER COLUMN user_id DROP NOT NULL, + ALTER COLUMN filter_id DROP NOT NULL, + ALTER COLUMN whole_word DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191023_set_not_null_for_instances.exs b/priv/repo/migrations/20191026191023_set_not_null_for_instances.exs new file mode 100644 index 000000000..4c2560da5 --- /dev/null +++ b/priv/repo/migrations/20191026191023_set_not_null_for_instances.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForInstances do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE instances + ALTER COLUMN host SET NOT NULL") + end + + def down do + execute("ALTER TABLE instances + ALTER COLUMN host DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191100_set_not_null_for_lists.exs b/priv/repo/migrations/20191026191100_set_not_null_for_lists.exs new file mode 100644 index 000000000..40b8b136a --- /dev/null +++ b/priv/repo/migrations/20191026191100_set_not_null_for_lists.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForLists do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE lists + ALTER COLUMN user_id SET NOT NULL") + end + + def down do + execute("ALTER TABLE lists + ALTER COLUMN user_id DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191134_set_not_null_for_markers.exs b/priv/repo/migrations/20191026191134_set_not_null_for_markers.exs new file mode 100644 index 000000000..7d7b73e7b --- /dev/null +++ b/priv/repo/migrations/20191026191134_set_not_null_for_markers.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForMarkers do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE markers + ALTER COLUMN user_id SET NOT NULL") + end + + def down do + execute("ALTER TABLE markers + ALTER COLUMN user_id DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191218_set_not_null_for_moderation_log.exs b/priv/repo/migrations/20191026191218_set_not_null_for_moderation_log.exs new file mode 100644 index 000000000..7238ca7f5 --- /dev/null +++ b/priv/repo/migrations/20191026191218_set_not_null_for_moderation_log.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForModerationLog do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE moderation_log + ALTER COLUMN data SET NOT NULL") + end + + def down do + execute("ALTER TABLE moderation_log + ALTER COLUMN data DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191249_set_not_null_for_notifications.exs b/priv/repo/migrations/20191026191249_set_not_null_for_notifications.exs new file mode 100644 index 000000000..7e2976bab --- /dev/null +++ b/priv/repo/migrations/20191026191249_set_not_null_for_notifications.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForNotifications do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE notifications + ALTER COLUMN user_id SET NOT NULL, + ALTER COLUMN seen SET NOT NULL") + end + + def down do + execute("ALTER TABLE notifications + ALTER COLUMN user_id DROP NOT NULL, + ALTER COLUMN seen DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191328_set_not_null_for_oauth_authorizations.exs b/priv/repo/migrations/20191026191328_set_not_null_for_oauth_authorizations.exs new file mode 100644 index 000000000..bc6b36e69 --- /dev/null +++ b/priv/repo/migrations/20191026191328_set_not_null_for_oauth_authorizations.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForOauthAuthorizations do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE oauth_authorizations + ALTER COLUMN app_id SET NOT NULL, + ALTER COLUMN token SET NOT NULL, + ALTER COLUMN used SET NOT NULL") + end + + def down do + execute("ALTER TABLE oauth_authorizations + ALTER COLUMN app_id DROP NOT NULL, + ALTER COLUMN token DROP NOT NULL, + ALTER COLUMN used DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191401_set_not_null_for_oauth_tokens.exs b/priv/repo/migrations/20191026191401_set_not_null_for_oauth_tokens.exs new file mode 100644 index 000000000..fe67db8cc --- /dev/null +++ b/priv/repo/migrations/20191026191401_set_not_null_for_oauth_tokens.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForOauthTokens do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE oauth_tokens + ALTER COLUMN app_id SET NOT NULL") + end + + def down do + execute("ALTER TABLE oauth_tokens + ALTER COLUMN app_id DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191442_set_not_null_for_objects.exs b/priv/repo/migrations/20191026191442_set_not_null_for_objects.exs new file mode 100644 index 000000000..59e89d6da --- /dev/null +++ b/priv/repo/migrations/20191026191442_set_not_null_for_objects.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForObjects do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE objects + ALTER COLUMN data SET NOT NULL") + end + + def down do + execute("ALTER TABLE objects + ALTER COLUMN data DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191524_set_not_null_for_password_reset_tokens.exs b/priv/repo/migrations/20191026191524_set_not_null_for_password_reset_tokens.exs new file mode 100644 index 000000000..51efadbd3 --- /dev/null +++ b/priv/repo/migrations/20191026191524_set_not_null_for_password_reset_tokens.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForPasswordResetTokens do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE password_reset_tokens + ALTER COLUMN token SET NOT NULL, + ALTER COLUMN user_id SET NOT NULL, + ALTER COLUMN used SET NOT NULL") + end + + def down do + execute("ALTER TABLE password_reset_tokens + ALTER COLUMN token DROP NOT NULL, + ALTER COLUMN user_id DROP NOT NULL, + ALTER COLUMN used DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191603_set_not_null_for_push_subscriptions.exs b/priv/repo/migrations/20191026191603_set_not_null_for_push_subscriptions.exs new file mode 100644 index 000000000..0f3a73067 --- /dev/null +++ b/priv/repo/migrations/20191026191603_set_not_null_for_push_subscriptions.exs @@ -0,0 +1,25 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForPushSubscriptions do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE push_subscriptions + ALTER COLUMN user_id SET NOT NULL, + ALTER COLUMN token_id SET NOT NULL, + ALTER COLUMN endpoint SET NOT NULL, + ALTER COLUMN key_p256dh SET NOT NULL, + ALTER COLUMN key_auth SET NOT NULL, + ALTER COLUMN data SET NOT NULL") + end + + def down do + execute("ALTER TABLE push_subscriptions + ALTER COLUMN user_id DROP NOT NULL, + ALTER COLUMN token_id DROP NOT NULL, + ALTER COLUMN endpoint DROP NOT NULL, + ALTER COLUMN key_p256dh DROP NOT NULL, + ALTER COLUMN key_auth DROP NOT NULL, + ALTER COLUMN data DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191635_set_not_null_for_registrations.exs b/priv/repo/migrations/20191026191635_set_not_null_for_registrations.exs new file mode 100644 index 000000000..ddfbf4c5e --- /dev/null +++ b/priv/repo/migrations/20191026191635_set_not_null_for_registrations.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForRegistrations do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE registrations + ALTER COLUMN provider SET NOT NULL, + ALTER COLUMN uid SET NOT NULL, + ALTER COLUMN info SET NOT NULL") + end + + def down do + execute("ALTER TABLE registrations + ALTER COLUMN provider DROP NOT NULL, + ALTER COLUMN uid DROP NOT NULL, + ALTER COLUMN info DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191711_set_not_null_for_scheduled_activities.exs b/priv/repo/migrations/20191026191711_set_not_null_for_scheduled_activities.exs new file mode 100644 index 000000000..f1830c8c3 --- /dev/null +++ b/priv/repo/migrations/20191026191711_set_not_null_for_scheduled_activities.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForScheduledActivities do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE scheduled_activities + ALTER COLUMN user_id SET NOT NULL") + end + + def down do + execute("ALTER TABLE scheduled_activities + ALTER COLUMN user_id DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191753_set_not_null_for_thread_mutes.exs b/priv/repo/migrations/20191026191753_set_not_null_for_thread_mutes.exs new file mode 100644 index 000000000..daa7ce314 --- /dev/null +++ b/priv/repo/migrations/20191026191753_set_not_null_for_thread_mutes.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForThreadMutes do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE thread_mutes + ALTER COLUMN user_id SET NOT NULL, + ALTER COLUMN context SET NOT NULL") + end + + def down do + execute("ALTER TABLE thread_mutes + ALTER COLUMN user_id DROP NOT NULL, + ALTER COLUMN context DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191826_set_not_null_for_user_invite_tokens.exs b/priv/repo/migrations/20191026191826_set_not_null_for_user_invite_tokens.exs new file mode 100644 index 000000000..836544f7f --- /dev/null +++ b/priv/repo/migrations/20191026191826_set_not_null_for_user_invite_tokens.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForUserInviteTokens do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + execute("ALTER TABLE user_invite_tokens + ALTER COLUMN used SET NOT NULL, + ALTER COLUMN uses SET NOT NULL, + ALTER COLUMN invite_type SET NOT NULL") + end + + def down do + execute("ALTER TABLE user_invite_tokens + ALTER COLUMN used DROP NOT NULL, + ALTER COLUMN uses DROP NOT NULL, + ALTER COLUMN invite_type DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191026191910_set_not_null_for_users.exs b/priv/repo/migrations/20191026191910_set_not_null_for_users.exs new file mode 100644 index 000000000..9d8d0ccf8 --- /dev/null +++ b/priv/repo/migrations/20191026191910_set_not_null_for_users.exs @@ -0,0 +1,44 @@ +defmodule Pleroma.Repo.Migrations.SetNotNullForUsers do + use Ecto.Migration + + # modify/3 function will require index recreation, so using execute/1 instead + + def up do + # irreversible + execute("UPDATE users SET follower_count = 0 WHERE follower_count IS NULL") + + execute("ALTER TABLE users + ALTER COLUMN local SET NOT NULL, + ALTER COLUMN source_data SET NOT NULL, + ALTER COLUMN note_count SET NOT NULL, + ALTER COLUMN follower_count SET NOT NULL, + ALTER COLUMN blocks SET NOT NULL, + ALTER COLUMN domain_blocks SET NOT NULL, + ALTER COLUMN mutes SET NOT NULL, + ALTER COLUMN muted_reblogs SET NOT NULL, + ALTER COLUMN muted_notifications SET NOT NULL, + ALTER COLUMN subscribers SET NOT NULL, + ALTER COLUMN pinned_activities SET NOT NULL, + ALTER COLUMN emoji SET NOT NULL, + ALTER COLUMN fields SET NOT NULL, + ALTER COLUMN raw_fields SET NOT NULL") + end + + def down do + execute("ALTER TABLE users + ALTER COLUMN local DROP NOT NULL, + ALTER COLUMN source_data DROP NOT NULL, + ALTER COLUMN note_count DROP NOT NULL, + ALTER COLUMN follower_count DROP NOT NULL, + ALTER COLUMN blocks DROP NOT NULL, + ALTER COLUMN domain_blocks DROP NOT NULL, + ALTER COLUMN mutes DROP NOT NULL, + ALTER COLUMN muted_reblogs DROP NOT NULL, + ALTER COLUMN muted_notifications DROP NOT NULL, + ALTER COLUMN subscribers DROP NOT NULL, + ALTER COLUMN pinned_activities DROP NOT NULL, + ALTER COLUMN emoji DROP NOT NULL, + ALTER COLUMN fields DROP NOT NULL, + ALTER COLUMN raw_fields DROP NOT NULL") + end +end diff --git a/priv/repo/migrations/20191029101340_migrate_missing_follow_requests.exs b/priv/repo/migrations/20191029101340_migrate_missing_follow_requests.exs new file mode 100644 index 000000000..90b18efc8 --- /dev/null +++ b/priv/repo/migrations/20191029101340_migrate_missing_follow_requests.exs @@ -0,0 +1,35 @@ +defmodule Pleroma.Repo.Migrations.MigrateMissingFollowingRelationships do + use Ecto.Migration + + def change do + execute(import_pending_follows_from_activities(), "") + end + + defp import_pending_follows_from_activities do + """ + INSERT INTO + following_relationships ( + follower_id, + following_id, + state, + inserted_at, + updated_at + ) + SELECT + followers.id, + following.id, + activities.data ->> 'state', + (activities.data ->> 'published') :: timestamp, + now() + FROM + activities + JOIN users AS followers ON (activities.actor = followers.ap_id) + JOIN users AS following ON (activities.data ->> 'object' = following.ap_id) + WHERE + activities.data ->> 'type' = 'Follow' + AND activities.data ->> 'state' = 'pending' + ORDER BY activities.updated_at DESC + ON CONFLICT DO NOTHING + """ + end +end diff --git a/priv/repo/migrations/20191029172832_fix_blocked_follows.exs b/priv/repo/migrations/20191029172832_fix_blocked_follows.exs new file mode 100644 index 000000000..71f8f1330 --- /dev/null +++ b/priv/repo/migrations/20191029172832_fix_blocked_follows.exs @@ -0,0 +1,112 @@ +defmodule Pleroma.Repo.Migrations.FixBlockedFollows do + use Ecto.Migration + + import Ecto.Query + alias Pleroma.Config + alias Pleroma.Repo + + def up do + unfollow_blocked = Config.get([:activitypub, :unfollow_blocked]) + + if unfollow_blocked do + "activities" + |> where([activity], fragment("? ->> 'type' = 'Block'", activity.data)) + |> distinct([activity], [ + activity.actor, + fragment( + "coalesce((?)->'object'->>'id', (?)->>'object')", + activity.data, + activity.data + ) + ]) + |> order_by([activity], [fragment("? desc nulls last", activity.id)]) + |> select([activity], %{ + blocker: activity.actor, + blocked: + fragment("coalesce((?)->'object'->>'id', (?)->>'object')", activity.data, activity.data), + created_at: activity.id + }) + |> Repo.stream() + |> Enum.map(&unfollow_if_blocked/1) + |> Enum.uniq() + |> Enum.each(&update_follower_count/1) + end + end + + def down do + end + + def unfollow_if_blocked(%{blocker: blocker_id, blocked: blocked_id, created_at: blocked_at}) do + query = + from( + activity in "activities", + where: fragment("? ->> 'type' = 'Follow'", activity.data), + where: activity.actor == ^blocked_id, + # this is to use the index + where: + fragment( + "coalesce((?)->'object'->>'id', (?)->>'object') = ?", + activity.data, + activity.data, + ^blocker_id + ), + where: activity.id > ^blocked_at, + where: fragment("(?)->>'state' = 'accept'", activity.data), + order_by: [fragment("? desc nulls last", activity.id)] + ) + + unless Repo.exists?(query) do + blocker = "users" |> select([:id, :local]) |> Repo.get_by(ap_id: blocker_id) + blocked = "users" |> select([:id]) |> Repo.get_by(ap_id: blocked_id) + + if !is_nil(blocker) && !is_nil(blocked) do + unfollow(blocked, blocker) + end + end + end + + def unfollow(%{id: follower_id}, %{id: followed_id} = followed) do + following_relationship = + "following_relationships" + |> where(follower_id: ^follower_id, following_id: ^followed_id, state: "accept") + |> select([:id]) + |> Repo.one() + + case following_relationship do + nil -> + {:ok, nil} + + %{id: following_relationship_id} -> + "following_relationships" + |> where(id: ^following_relationship_id) + |> Repo.delete_all() + + followed + end + end + + def update_follower_count(%{id: user_id} = user) do + if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do + follower_count_query = + "users" + |> where([u], u.id != ^user_id) + |> where([u], u.deactivated != ^true) + |> join(:inner, [u], r in "following_relationships", + as: :relationships, + on: r.following_id == ^user_id and r.follower_id == u.id + ) + |> where([relationships: r], r.state == "accept") + |> select([u], %{count: count(u.id)}) + + "users" + |> where(id: ^user_id) + |> join(:inner, [u], s in subquery(follower_count_query)) + |> update([u, s], + set: [follower_count: s.count] + ) + |> Repo.update_all([]) + end + end + + def update_follower_count(_), do: :noop +end diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index 96665844e..8bae42f6d 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -19,6 +19,7 @@ "value": "schema:value", "sensitive": "as:sensitive", "litepub": "http://litepub.social/ns#", + "invisible": "litepub:invisible", "directMessage": "litepub:directMessage", "listMessage": { "@id": "litepub:listMessage", diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl index e731d20eb..87c486514 100755 --- a/rel/files/bin/pleroma_ctl +++ b/rel/files/bin/pleroma_ctl @@ -35,31 +35,62 @@ detect_branch() { if [ "$branch" = "develop" ]; then echo "develop" elif [ "$branch" = "" ]; then - echo "master" + echo "stable" else - # Note: branch name in version is of SemVer format and may only contain [0-9a-zA-Z-] symbols — - # if supporting releases for more branches, need to ensure they contain only these symbols. - echo "Releases are built only for master and develop branches" >&2 + # Note: branch name in version is of SemVer format and may only contain [0-9a-zA-Z-] symbols — + # if supporting releases for more branches, need to ensure they contain only these symbols. + echo "Can't detect the branch automatically, please specify it by using the --branch option." >&2 exit 1 fi } update() { set -e + NO_RM=false + + while echo "$1" | grep "^-" >/dev/null; do + case "$1" in + --zip-url) + FULL_URI="$2" + shift 2 + ;; + --no-rm) + NO_RM=true + shift + ;; + --flavour) + FLAVOUR="$2" + shift 2 + ;; + --branch) + BRANCH="$2" + shift 2 + ;; + --tmp-dir) + TMP_DIR="$2" + shift 2 + ;; + -*) + echo "invalid option: $1" 1>&2 + shift + ;; + esac + done + RELEASE_ROOT=$(dirname "$SCRIPTPATH") - uri="${PLEROMA_CTL_URI:-https://git.pleroma.social}" - project_id="${PLEROMA_CTL_PROJECT_ID:-2}" - project_branch="$(detect_branch)" - flavour="${PLEROMA_CTL_FLAVOUR:-$(detect_flavour)}" - echo "Detected flavour: $flavour" - tmp="${PLEROMA_CTL_TMP_DIR:-/tmp}" + uri="https://git.pleroma.social" + project_id="2" + project_branch="${BRANCH:-$(detect_branch)}" + flavour="${FLAVOUR:-$(detect_flavour)}" + tmp="${TMP_DIR:-/tmp}" artifact="$tmp/pleroma.zip" - full_uri="${uri}/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=${flavour}" + full_uri="${FULL_URI:-${uri}/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=${flavour}}" echo "Downloading the artifact from ${full_uri} to ${artifact}" curl "$full_uri" -o "${artifact}" echo "Unpacking ${artifact} to ${tmp}" unzip -q "$artifact" -d "$tmp" echo "Copying files over to $RELEASE_ROOT" - if [ "$1" != "--no-rm" ]; then + if [ "$NO_RM" = false ]; then + echo "Removing files from the previous release" rm -r "${RELEASE_ROOT:-?}"/* fi cp -rf "$tmp/release"/* "$RELEASE_ROOT" @@ -86,36 +117,45 @@ if [ -z "$1" ] || [ "$1" = "help" ]; then Rollback database migrations (needs to be done before downgrading) update [OPTIONS] - Update the instance using the latest CI artifact for the current branch. - - The only supported option is --no-rm, when set the script won't delete the whole directory, but - just force copy over files from the new release. This wastes more space, but may be useful if - some files are stored inside of the release directories (although you really shouldn't store them - there), or if you want to be able to quickly revert a broken update. - - The script will try to detect your architecture and ABI and set a flavour automatically, - but if it is wrong, you can overwrite it by setting PLEROMA_CTL_FLAVOUR to the desired flavour. - - By default the artifact will be downloaded from https://git.pleroma.social for pleroma/pleroma (project id: 2) - to /tmp/, you can overwrite these settings by setting PLEROMA_CTL_URI, PLEROMA_CTL_PROJECT_ID and PLEROMA_CTL_TMP_DIR - respectively. + Update the instance. + Options: + --branch Update to a specified branch, instead of the latest version of the current one. + --flavour Update to a specified flavour (CPU architecture+libc), instead of the current one. + --zip-url Get the release from a specified url. If set, renders the previous 2 options inactive. + --no-rm Do not erase previous release's files. + --tmp-dir Download the temporary files to a specified directory. and any mix tasks under Pleroma namespace, for example \`mix pleroma.user COMMAND\` is equivalent to \`$(basename "$0") user COMMAND\` By default pleroma_ctl will try calling into a running instance to execute non migration-related commands, - if for some reason this is undesired, set PLEROMA_CTL_RPC_DISABLED environment variable + if for some reason this is undesired, set PLEROMA_CTL_RPC_DISABLED environment variable. + " else SCRIPT=$(readlink -f "$0") SCRIPTPATH=$(dirname "$SCRIPT") - if [ "$1" = "update" ]; then - update "$2" - elif [ "$1" = "migrate" ] || [ "$1" = "rollback" ] || [ "$1" = "create" ] || [ "$1 $2" = "instance gen" ] || [ -n "$PLEROMA_CTL_RPC_DISABLED" ]; then - "$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run("'"$*"'")' + FULL_ARGS="$*" + + ACTION="$1" + if [ $# -gt 0 ]; then + shift + fi + echo "$1" | grep "^-" >/dev/null + if [ $? -eq 1 ]; then + SUBACTION="$1" + if [ $# -gt 0 ]; then + shift + fi + fi + + if [ "$ACTION" = "update" ]; then + update "$@" + elif [ "$ACTION" = "migrate" ] || [ "$ACTION" = "rollback" ] || [ "$ACTION" = "create" ] || [ "$ACTION $SUBACTION" = "instance gen" ] || [ "$PLEROMA_CTL_RPC_DISABLED" = true ]; then + "$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")' else - "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run("'"$*"'")' + "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")' fi fi diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs index f430bdf75..863270022 100644 --- a/test/conversation/participation_test.exs +++ b/test/conversation/participation_test.exs @@ -23,6 +23,39 @@ test "getting a participation will also preload things" do assert %Pleroma.Conversation{} = participation.conversation end + test "for a new conversation or a reply, it doesn't mark the author's participation as unread" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _} = + CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"}) + + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + [%{read: true}] = Participation.for_user(user) + [%{read: false} = participation] = Participation.for_user(other_user) + + assert User.get_cached_by_id(user.id).unread_conversation_count == 0 + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 1 + + {:ok, _} = + CommonAPI.post(other_user, %{ + "status" => "Hey @#{user.nickname}.", + "visibility" => "direct", + "in_reply_to_conversation_id" => participation.id + }) + + user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) + + [%{read: false}] = Participation.for_user(user) + [%{read: true}] = Participation.for_user(other_user) + + assert User.get_cached_by_id(user.id).unread_conversation_count == 1 + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 + end + test "for a new conversation, it sets the recipents of the participation" do user = insert(:user) other_user = insert(:user) @@ -32,7 +65,7 @@ test "for a new conversation, it sets the recipents of the participation" do CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"}) user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(user.id) + other_user = User.get_cached_by_id(other_user.id) [participation] = Participation.for_user(user) participation = Pleroma.Repo.preload(participation, :recipients) @@ -100,6 +133,20 @@ test "it marks a participation as unread" do refute participation.read end + test "it marks all the user's participations as read" do + user = insert(:user) + other_user = insert(:user) + participation1 = insert(:participation, %{read: false, user: user}) + participation2 = insert(:participation, %{read: false, user: user}) + participation3 = insert(:participation, %{read: false, user: other_user}) + + {:ok, _, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user) + + assert Participation.get(participation1.id).read == true + assert Participation.get(participation2.id).read == true + assert Participation.get(participation3.id).read == false + end + test "gets all the participations for a user, ordered by updated at descending" do user = insert(:user) {:ok, activity_one} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"}) @@ -169,4 +216,134 @@ test "it sets recipients, always keeping the owner of the participation even whe assert user in participation.recipients assert other_user in participation.recipients end + + describe "blocking" do + test "when the user blocks a recipient, the existing conversations with them are marked as read" do + blocker = insert(:user) + blocked = insert(:user) + third_user = insert(:user) + + {:ok, _direct1} = + CommonAPI.post(third_user, %{ + "status" => "Hi @#{blocker.nickname}", + "visibility" => "direct" + }) + + {:ok, _direct2} = + CommonAPI.post(third_user, %{ + "status" => "Hi @#{blocker.nickname}, @#{blocked.nickname}", + "visibility" => "direct" + }) + + {:ok, _direct3} = + CommonAPI.post(blocked, %{ + "status" => "Hi @#{blocker.nickname}", + "visibility" => "direct" + }) + + {:ok, _direct4} = + CommonAPI.post(blocked, %{ + "status" => "Hi @#{blocker.nickname}, @#{third_user.nickname}", + "visibility" => "direct" + }) + + assert [%{read: false}, %{read: false}, %{read: false}, %{read: false}] = + Participation.for_user(blocker) + + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4 + + {:ok, blocker} = User.block(blocker, blocked) + + # The conversations with the blocked user are marked as read + assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] = + Participation.for_user(blocker) + + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 1 + + # The conversation is not marked as read for the blocked user + assert [_, _, %{read: false}] = Participation.for_user(blocked) + assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1 + + # The conversation is not marked as read for the third user + assert [%{read: false}, _, _] = Participation.for_user(third_user) + assert User.get_cached_by_id(third_user.id).unread_conversation_count == 1 + end + + test "the new conversation with the blocked user is not marked as unread " do + blocker = insert(:user) + blocked = insert(:user) + third_user = insert(:user) + + {:ok, blocker} = User.block(blocker, blocked) + + # When the blocked user is the author + {:ok, _direct1} = + CommonAPI.post(blocked, %{ + "status" => "Hi @#{blocker.nickname}", + "visibility" => "direct" + }) + + assert [%{read: true}] = Participation.for_user(blocker) + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 + + # When the blocked user is a recipient + {:ok, _direct2} = + CommonAPI.post(third_user, %{ + "status" => "Hi @#{blocker.nickname}, @#{blocked.nickname}", + "visibility" => "direct" + }) + + assert [%{read: true}, %{read: true}] = Participation.for_user(blocker) + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 + + assert [%{read: false}, _] = Participation.for_user(blocked) + assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1 + end + + test "the conversation with the blocked user is not marked as unread on a reply" do + blocker = insert(:user) + blocked = insert(:user) + third_user = insert(:user) + + {:ok, _direct1} = + CommonAPI.post(blocker, %{ + "status" => "Hi @#{third_user.nickname}, @#{blocked.nickname}", + "visibility" => "direct" + }) + + {:ok, blocker} = User.block(blocker, blocked) + assert [%{read: true}] = Participation.for_user(blocker) + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 + + assert [blocked_participation] = Participation.for_user(blocked) + + # When it's a reply from the blocked user + {:ok, _direct2} = + CommonAPI.post(blocked, %{ + "status" => "reply", + "visibility" => "direct", + "in_reply_to_conversation_id" => blocked_participation.id + }) + + assert [%{read: true}] = Participation.for_user(blocker) + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 + + assert [third_user_participation] = Participation.for_user(third_user) + + # When it's a reply from the third user + {:ok, _direct3} = + CommonAPI.post(third_user, %{ + "status" => "reply", + "visibility" => "direct", + "in_reply_to_conversation_id" => third_user_participation.id + }) + + assert [%{read: true}] = Participation.for_user(blocker) + assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 + + # Marked as unread for the blocked user + assert [%{read: false}] = Participation.for_user(blocked) + assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1 + end + end end diff --git a/test/daemons/digest_email_daemon_test.exs b/test/daemons/digest_email_daemon_test.exs index 3168f3b9a..faf592d5f 100644 --- a/test/daemons/digest_email_daemon_test.exs +++ b/test/daemons/digest_email_daemon_test.exs @@ -20,7 +20,7 @@ test "it sends digest emails" do |> Timex.to_naive_datetime() user2 = insert(:user, last_digest_emailed_at: date) - User.switch_email_notifications(user2, "digest", true) + {:ok, _} = User.switch_email_notifications(user2, "digest", true) CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}!"}) DigestEmailDaemon.perform() diff --git a/test/emails/user_email_test.exs b/test/emails/user_email_test.exs index 963565f7c..9e145977e 100644 --- a/test/emails/user_email_test.exs +++ b/test/emails/user_email_test.exs @@ -36,7 +36,7 @@ test "build user invitation email" do test "build account confirmation email" do config = Pleroma.Config.get(:instance) - user = insert(:user, info: %Pleroma.User.Info{confirmation_token: "conf-token"}) + user = insert(:user, confirmation_token: "conf-token") email = UserEmail.account_confirmation_email(user) assert email.from == {config[:name], config[:notify_email]} assert email.to == [{user.name, user.email}] diff --git a/test/emoji/formatter_test.exs b/test/emoji/formatter_test.exs index 6d25fc453..fda80d470 100644 --- a/test/emoji/formatter_test.exs +++ b/test/emoji/formatter_test.exs @@ -12,7 +12,7 @@ test "it adds cool emoji" do text = "I love :firefox:" expected_result = - "I love \"firefox\"" + "I love \"firefox\"" assert Formatter.emojify(text) == expected_result end @@ -28,10 +28,7 @@ test "it does not add XSS emoji" do } |> Pleroma.Emoji.build() - expected_result = - "I love \"\"" - - assert Formatter.emojify(text, [{custom_emoji.code, custom_emoji}]) == expected_result + refute Formatter.emojify(text, [{custom_emoji.code, custom_emoji}]) =~ text end end diff --git a/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json b/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json new file mode 100644 index 000000000..4b7b4df44 --- /dev/null +++ b/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://shitposter.club/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://shitposter.club/users/moonman","attachment":[],"attributedTo":"https://shitposter.club/users/moonman","cc":["https://shitposter.club/users/moonman/followers"],"content":"@neimzr4luzerz @dolus childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English","context":"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26","conversation":"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26","id":"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment","inReplyTo":"tag:shitposter.club,2017-05-05:noticeId=2827849:objectType=comment","inReplyToStatusId":2827849,"published":"2017-05-05T08:51:48Z","sensitive":false,"summary":null,"tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Note"} \ No newline at end of file diff --git a/test/fixtures/tesla_mock/moonman@shitposter.club.json b/test/fixtures/tesla_mock/moonman@shitposter.club.json new file mode 100644 index 000000000..8f9ced1dd --- /dev/null +++ b/test/fixtures/tesla_mock/moonman@shitposter.club.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://shitposter.club/schemas/litepub-0.1.jsonld",{"@language":"und"}],"attachment":[],"endpoints":{"oauthAuthorizationEndpoint":"https://shitposter.club/oauth/authorize","oauthRegistrationEndpoint":"https://shitposter.club/api/v1/apps","oauthTokenEndpoint":"https://shitposter.club/oauth/token","sharedInbox":"https://shitposter.club/inbox"},"followers":"https://shitposter.club/users/moonman/followers","following":"https://shitposter.club/users/moonman/following","icon":{"type":"Image","url":"https://shitposter.club/media/bda6e00074f6a02cbf32ddb0abec08151eb4c795e580927ff7ad638d00cde4c8.jpg?name=blob.jpg"},"id":"https://shitposter.club/users/moonman","image":{"type":"Image","url":"https://shitposter.club/media/4eefb90d-cdb2-2b4f-5f29-7612856a99d2/4eefb90d-cdb2-2b4f-5f29-7612856a99d2.jpeg"},"inbox":"https://shitposter.club/users/moonman/inbox","manuallyApprovesFollowers":false,"name":"Captain Howdy","outbox":"https://shitposter.club/users/moonman/outbox","preferredUsername":"moonman","publicKey":{"id":"https://shitposter.club/users/moonman#main-key","owner":"https://shitposter.club/users/moonman","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnOTitJ19ZqcOZHwSXQUM\nJq9ip4GNblp83LgwG1t5c2h2iaI3fXMsB4EaEBs8XHsoSFyDeDNRSPE3mtVgOnWv\n1eaXWMDerBT06th6DrElD9k5IoEPtZRY4HtZa1xGnte7+6RjuPOzZ1fR9C8WxGgi\nwb9iOUMhazpo85fC3iKCAL5XhiuA3Nas57MDJgueeI9BF+2oFelFZdMSWwG96uch\niDfp8nfpkmzYI6SWbylObjm8RsfZbGTosLHwWyJPEITeYI/5M0XwJe9dgVI1rVNU\n52kplWOGTo1rm6V0AMHaYAd9RpiXxe8xt5OeranrsE/5LvEQUl0fz7SE36YmsOaH\nTwIDAQAB\n-----END PUBLIC KEY-----\n\n"},"summary":"EMAIL:shitposterclub@gmail.com
XMPP: moon@talk.shitposter.club
PRONOUNS: none of your business

Purported leftist kike piece of shit","tag":[],"type":"Person","url":"https://shitposter.club/users/moonman"} \ No newline at end of file diff --git a/test/fixtures/tesla_mock/relay@mastdon.example.org.json b/test/fixtures/tesla_mock/relay@mastdon.example.org.json new file mode 100644 index 000000000..c1fab7d3b --- /dev/null +++ b/test/fixtures/tesla_mock/relay@mastdon.example.org.json @@ -0,0 +1,55 @@ +{ + "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "movedTo": "as:movedTo", + "Hashtag": "as:Hashtag", + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji" + }], + "id": "http://mastodon.example.org/users/admin", + "type": "Application", + "invisible": true, + "following": "http://mastodon.example.org/users/admin/following", + "followers": "http://mastodon.example.org/users/admin/followers", + "inbox": "http://mastodon.example.org/users/admin/inbox", + "outbox": "http://mastodon.example.org/users/admin/outbox", + "preferredUsername": "admin", + "name": null, + "summary": "\u003cp\u003e\u003c/p\u003e", + "url": "http://mastodon.example.org/@admin", + "manuallyApprovesFollowers": false, + "publicKey": { + "id": "http://mastodon.example.org/users/admin#main-key", + "owner": "http://mastodon.example.org/users/admin", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "attachment": [{ + "type": "PropertyValue", + "name": "foo", + "value": "bar" + }, + { + "type": "PropertyValue", + "name": "foo1", + "value": "bar1" + } + ], + "endpoints": { + "sharedInbox": "http://mastodon.example.org/inbox" + }, + "icon": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" + }, + "image": { + "type": "Image", + "mediaType": "image/png", + "url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" + } +} diff --git a/test/formatter_test.exs b/test/formatter_test.exs index 3bff51527..087bdbcc2 100644 --- a/test/formatter_test.exs +++ b/test/formatter_test.exs @@ -125,10 +125,10 @@ test "gives a replacement for user links, using local nicknames in user links te gsimg = insert(:user, %{nickname: "gsimg"}) archaeme = - insert(:user, %{ + insert(:user, nickname: "archa_eme_", - info: %User.Info{source_data: %{"url" => "https://archeme/@archa_eme_"}} - }) + source_data: %{"url" => "https://archeme/@archa_eme_"} + ) archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"}) diff --git a/test/html_test.exs b/test/html_test.exs index 306ad3b3b..f0869534c 100644 --- a/test/html_test.exs +++ b/test/html_test.exs @@ -21,31 +21,31 @@ defmodule Pleroma.HTMLTest do """ @html_onerror_sample """ - + """ @html_span_class_sample """ - hi + hi """ @html_span_microformats_sample """ - @foo + @foo """ @html_span_invalid_microformats_sample """ - @foo + @foo """ describe "StripTags scrubber" do test "works as expected" do expected = """ - this is in bold + this is in bold this is a paragraph this is a linebreak - this is a link with allowed "rel" attribute: example.com - this is a link with not allowed "rel" attribute: example.com + this is a link with allowed "rel" attribute: example.com + this is a link with not allowed "rel" attribute: example.com this is an image: - alert('hacked') + alert('hacked') """ assert expected == HTML.strip_tags(@html_sample) @@ -61,13 +61,13 @@ test "does not allow attribute-based XSS" do describe "TwitterText scrubber" do test "normalizes HTML as expected" do expected = """ - this is in bold + this is in bold

this is a paragraph

- this is a linebreak
- this is a link with allowed "rel" attribute: - this is a link with not allowed "rel" attribute: example.com - this is an image:
- alert('hacked') + this is a linebreak
+ this is a link with allowed "rel" attribute: + this is a link with not allowed "rel" attribute: example.com + this is an image:
+ alert('hacked') """ assert expected == HTML.filter_tags(@html_sample, Pleroma.HTML.Scrubber.TwitterText) @@ -75,7 +75,7 @@ test "normalizes HTML as expected" do test "does not allow attribute-based XSS" do expected = """ - + """ assert expected == HTML.filter_tags(@html_onerror_sample, Pleroma.HTML.Scrubber.TwitterText) @@ -115,13 +115,13 @@ test "filters invalid microformats markup" do describe "default scrubber" do test "normalizes HTML as expected" do expected = """ - this is in bold + this is in bold

this is a paragraph

- this is a linebreak
- this is a link with allowed "rel" attribute: - this is a link with not allowed "rel" attribute: example.com - this is an image:
- alert('hacked') + this is a linebreak
+ this is a link with allowed "rel" attribute: + this is a link with not allowed "rel" attribute: example.com + this is an image:
+ alert('hacked') """ assert expected == HTML.filter_tags(@html_sample, Pleroma.HTML.Scrubber.Default) @@ -129,7 +129,7 @@ test "normalizes HTML as expected" do test "does not allow attribute-based XSS" do expected = """ - + """ assert expected == HTML.filter_tags(@html_onerror_sample, Pleroma.HTML.Scrubber.Default) diff --git a/test/marker_test.exs b/test/marker_test.exs new file mode 100644 index 000000000..04bd67fe6 --- /dev/null +++ b/test/marker_test.exs @@ -0,0 +1,51 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MarkerTest do + use Pleroma.DataCase + alias Pleroma.Marker + + import Pleroma.Factory + + describe "get_markers/2" do + test "returns user markers" do + user = insert(:user) + marker = insert(:marker, user: user) + insert(:marker, timeline: "home", user: user) + assert Marker.get_markers(user, ["notifications"]) == [refresh_record(marker)] + end + end + + describe "upsert/2" do + test "creates a marker" do + user = insert(:user) + + {:ok, %{"notifications" => %Marker{} = marker}} = + Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "34"}} + ) + + assert marker.timeline == "notifications" + assert marker.last_read_id == "34" + assert marker.lock_version == 0 + end + + test "updates exist marker" do + user = insert(:user) + marker = insert(:marker, user: user, last_read_id: "8909") + + {:ok, %{"notifications" => %Marker{}}} = + Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "9909"}} + ) + + marker = refresh_record(marker) + assert marker.timeline == "notifications" + assert marker.last_read_id == "9909" + assert marker.lock_version == 0 + end + end +end diff --git a/test/moderation_log_test.exs b/test/moderation_log_test.exs index a39a00e02..4240f6a65 100644 --- a/test/moderation_log_test.exs +++ b/test/moderation_log_test.exs @@ -12,8 +12,8 @@ defmodule Pleroma.ModerationLogTest do describe "user moderation" do setup do - admin = insert(:user, info: %{is_admin: true}) - moderator = insert(:user, info: %{is_moderator: true}) + admin = insert(:user, is_admin: true) + moderator = insert(:user, is_moderator: true) subject1 = insert(:user) subject2 = insert(:user) @@ -24,13 +24,13 @@ test "logging user deletion by moderator", %{moderator: moderator, subject1: sub {:ok, _} = ModerationLog.insert_log(%{ actor: moderator, - subject: subject1, + subject: [subject1], action: "delete" }) log = Repo.one(ModerationLog) - assert log.data["message"] == "@#{moderator.nickname} deleted user @#{subject1.nickname}" + assert log.data["message"] == "@#{moderator.nickname} deleted users: @#{subject1.nickname}" end test "logging user creation by moderator", %{ @@ -128,7 +128,7 @@ test "logging user grant by moderator", %{moderator: moderator, subject1: subjec {:ok, _} = ModerationLog.insert_log(%{ actor: moderator, - subject: subject1, + subject: [subject1], action: "grant", permission: "moderator" }) @@ -142,7 +142,7 @@ test "logging user revoke by moderator", %{moderator: moderator, subject1: subje {:ok, _} = ModerationLog.insert_log(%{ actor: moderator, - subject: subject1, + subject: [subject1], action: "revoke", permission: "moderator" }) diff --git a/test/notification_test.exs b/test/notification_test.exs index 54c0f9877..f8d429223 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -136,7 +136,7 @@ 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, info: %{notification_settings: %{"followers" => false}}) + followed = insert(:user, notification_settings: %{"followers" => false}) User.follow(follower, followed) {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"}) refute Notification.create_notification(activity, followed) @@ -144,13 +144,13 @@ test "it disables notifications from followers" do test "it disables notifications from non-followers" do follower = insert(:user) - followed = insert(:user, info: %{notification_settings: %{"non_followers" => false}}) + followed = insert(:user, notification_settings: %{"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, info: %{notification_settings: %{"follows" => false}}) + follower = insert(:user, notification_settings: %{"follows" => false}) followed = insert(:user) User.follow(follower, followed) follower = Repo.get(User, follower.id) @@ -159,7 +159,7 @@ 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, info: %{notification_settings: %{"non_follows" => false}}) + follower = insert(:user, notification_settings: %{"non_follows" => false}) followed = insert(:user) {:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"}) refute Notification.create_notification(activity, follower) @@ -683,7 +683,7 @@ test "it doesn't return notifications for muted thread" do assert Notification.for_user(user) == [] end - test "it returns notifications for muted user with notifications and with_muted parameter" 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) @@ -693,27 +693,27 @@ test "it returns notifications for muted user with notifications and with_muted assert length(Notification.for_user(user, %{with_muted: true})) == 1 end - test "it returns notifications for blocked user and with_muted parameter" 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, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) - assert length(Notification.for_user(user, %{with_muted: true})) == 1 + assert length(Notification.for_user(user, %{with_muted: true})) == 0 end - test "it returns notificatitons for blocked domain and with_muted parameter" do + test "it doesn't return notifications from a domain-blocked user when with_muted is set" do user = insert(:user) blocked = insert(:user, ap_id: "http://some-domain.com") {:ok, user} = User.block_domain(user, "some-domain.com") {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) - assert length(Notification.for_user(user, %{with_muted: true})) == 1 + assert length(Notification.for_user(user, %{with_muted: true})) == 0 end - test "it returns notifications for muted thread with_muted parameter" do + test "it returns notifications from muted threads when with_muted is set" do user = insert(:user) another_user = insert(:user) diff --git a/test/object/containment_test.exs b/test/object/containment_test.exs index 61cd1b412..0dc2728b9 100644 --- a/test/object/containment_test.exs +++ b/test/object/containment_test.exs @@ -65,7 +65,7 @@ test "users cannot be collided through fake direction spoofing attempts" do assert capture_log(fn -> {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye") end) =~ - "[error] Could not decode user at fetch https://n1u.moe/users/rye, {:error, :error}" + "[error] Could not decode user at fetch https://n1u.moe/users/rye" end end diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs index 895a73d2c..9ae6b015d 100644 --- a/test/object/fetcher_test.exs +++ b/test/object/fetcher_test.exs @@ -27,31 +27,16 @@ defmodule Pleroma.Object.FetcherTest do end describe "actor origin containment" do - test_with_mock "it rejects objects with a bogus origin", - Pleroma.Web.OStatus, - [:passthrough], - [] do + test "it rejects objects with a bogus origin" do {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json") - - refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_)) end - test_with_mock "it rejects objects when attributedTo is wrong (variant 1)", - Pleroma.Web.OStatus, - [:passthrough], - [] do + test "it rejects objects when attributedTo is wrong (variant 1)" do {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json") - - refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_)) end - test_with_mock "it rejects objects when attributedTo is wrong (variant 2)", - Pleroma.Web.OStatus, - [:passthrough], - [] do + test "it rejects objects when attributedTo is wrong (variant 2)" do {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json") - - refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_)) end end @@ -71,24 +56,6 @@ test "it fetches an object" do assert object == object_again end - - test "it works with objects only available via Ostatus" do - {:ok, object} = Fetcher.fetch_object_from_id("https://shitposter.club/notice/2827873") - assert activity = Activity.get_create_by_object_ap_id(object.data["id"]) - assert activity.data["id"] - - {:ok, object_again} = Fetcher.fetch_object_from_id("https://shitposter.club/notice/2827873") - - assert object == object_again - end - - test "it correctly stitches up conversations between ostatus and ap" do - last = "https://mstdn.io/users/mayuutann/statuses/99568293732299394" - {:ok, object} = Fetcher.fetch_object_from_id(last) - - object = Object.get_by_ap_id(object.data["inReplyTo"]) - assert object - end end describe "implementation quirks" do diff --git a/test/object_test.exs b/test/object_test.exs index dd228c32f..9247a6d84 100644 --- a/test/object_test.exs +++ b/test/object_test.exs @@ -124,6 +124,8 @@ test "refetches if the time since the last refetch is greater than the interval" %Object{} = object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + Object.set_cache(object) + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 @@ -133,6 +135,8 @@ test "refetches if the time since the last refetch is greater than the interval" }) updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3 end @@ -141,6 +145,8 @@ test "returns the old object if refetch fails", %{mock_modified: mock_modified} %Object{} = object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + Object.set_cache(object) + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 @@ -148,6 +154,8 @@ test "returns the old object if refetch fails", %{mock_modified: mock_modified} mock_modified.(%Tesla.Env{status: 404, body: ""}) updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0 end) =~ @@ -160,6 +168,8 @@ test "does not refetch if the time since the last refetch is greater than the in %Object{} = object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + Object.set_cache(object) + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 @@ -169,6 +179,8 @@ test "does not refetch if the time since the last refetch is greater than the in }) updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0 end @@ -177,6 +189,8 @@ test "preserves internal fields on refetch", %{mock_modified: mock_modified} do %Object{} = object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d") + Object.set_cache(object) + assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4 assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0 @@ -192,6 +206,8 @@ test "preserves internal fields on refetch", %{mock_modified: mock_modified} do }) updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1) + object_in_cache = Object.get_cached_by_ap_id(object.data["id"]) + assert updated_object == object_in_cache assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8 assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3 diff --git a/test/plugs/admin_secret_authentication_plug_test.exs b/test/plugs/admin_secret_authentication_plug_test.exs index e1d4b391f..c94a62c10 100644 --- a/test/plugs/admin_secret_authentication_plug_test.exs +++ b/test/plugs/admin_secret_authentication_plug_test.exs @@ -37,6 +37,6 @@ test "with secret set and given in the 'admin_token' parameter, it assigns an ad %{conn | params: %{"admin_token" => "password123"}} |> AdminSecretAuthenticationPlug.call(%{}) - assert conn.assigns[:user].info.is_admin + assert conn.assigns[:user].is_admin end end diff --git a/test/plugs/cache_control_test.exs b/test/plugs/cache_control_test.exs index 69ce6cc7d..be78b3e1e 100644 --- a/test/plugs/cache_control_test.exs +++ b/test/plugs/cache_control_test.exs @@ -9,7 +9,7 @@ defmodule Pleroma.Web.CacheControlTest do test "Verify Cache-Control header on static assets", %{conn: conn} do conn = get(conn, "/index.html") - assert Conn.get_resp_header(conn, "cache-control") == ["public, no-cache"] + assert Conn.get_resp_header(conn, "cache-control") == ["public max-age=86400 must-revalidate"] end test "Verify Cache-Control header on the API", %{conn: conn} do diff --git a/test/plugs/user_enabled_plug_test.exs b/test/plugs/user_enabled_plug_test.exs index c0fafcab1..996a7d77b 100644 --- a/test/plugs/user_enabled_plug_test.exs +++ b/test/plugs/user_enabled_plug_test.exs @@ -17,7 +17,7 @@ test "doesn't do anything if the user isn't set", %{conn: conn} do end test "with a user that is deactivated, it removes that user", %{conn: conn} do - user = insert(:user, info: %{deactivated: true}) + user = insert(:user, deactivated: true) conn = conn diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs index 9e05fff18..136dcc54e 100644 --- a/test/plugs/user_is_admin_plug_test.exs +++ b/test/plugs/user_is_admin_plug_test.exs @@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do import Pleroma.Factory test "accepts a user that is admin" do - user = insert(:user, info: %{is_admin: true}) + user = insert(:user, is_admin: true) conn = build_conn() diff --git a/test/safe_jsonb_set_test.exs b/test/safe_jsonb_set_test.exs new file mode 100644 index 000000000..748540570 --- /dev/null +++ b/test/safe_jsonb_set_test.exs @@ -0,0 +1,12 @@ +defmodule Pleroma.SafeJsonbSetTest do + use Pleroma.DataCase + + test "it doesn't wipe the object when asked to set the value to NULL" do + assert %{rows: [[%{"key" => "value", "test" => nil}]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + "select safe_jsonb_set('{\"key\": \"value\"}'::jsonb, '{test}', NULL);", + [] + ) + end +end diff --git a/test/signature_test.exs b/test/signature_test.exs index 96c8ba07a..15cf10fb6 100644 --- a/test/signature_test.exs +++ b/test/signature_test.exs @@ -42,7 +42,7 @@ defp make_fake_conn(key_id), test "it returns key" do expected_result = {:ok, @rsa_public_key} - user = insert(:user, %{info: %{source_data: %{"publicKey" => @public_key}}}) + user = insert(:user, source_data: %{"publicKey" => @public_key}) assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result end @@ -54,7 +54,7 @@ test "it returns error when not found user" do end test "it returns error if public key is empty" do - user = insert(:user, %{info: %{source_data: %{"publicKey" => %{}}}}) + user = insert(:user, source_data: %{"publicKey" => %{}}) assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error} end @@ -69,8 +69,7 @@ test "it returns key" do test "it returns error when not found user" do assert capture_log(fn -> - assert Signature.refetch_public_key(make_fake_conn("test-ap_id")) == - {:error, {:error, :ok}} + {:error, _} = Signature.refetch_public_key(make_fake_conn("test-ap_id")) end) =~ "[error] Could not decode user" end end diff --git a/test/support/factory.ex b/test/support/factory.ex index b180844cd..e3f797f64 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -39,8 +39,7 @@ def user_factory do user | ap_id: User.ap_id(user), follower_address: User.ap_followers(user), - following_address: User.ap_following(user), - following: [User.ap_id(user)] + following_address: User.ap_following(user) } end @@ -281,26 +280,6 @@ def follow_activity_factory do } end - def websub_subscription_factory do - %Pleroma.Web.Websub.WebsubServerSubscription{ - topic: "http://example.org", - callback: "http://example.org/callback", - secret: "here's a secret", - valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 100), - state: "requested" - } - end - - def websub_client_subscription_factory do - %Pleroma.Web.Websub.WebsubClientSubscription{ - topic: "http://example.org", - secret: "here's a secret", - valid_until: nil, - state: "requested", - subscribers: [] - } - end - def oauth_app_factory do %Pleroma.Web.OAuth.App{ client_name: "Some client", @@ -397,4 +376,13 @@ def config_factory do ) } end + + def marker_factory do + %Pleroma.Marker{ + user: build(:user), + timeline: "notifications", + lock_version: 0, + last_read_id: "1" + } + end end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 4feb57f3a..965335e96 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -38,6 +38,14 @@ def get("https://osada.macgirvin.com/channel/mike", _, _, _) do }} end + def get("https://shitposter.club/users/moonman", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/moonman@shitposter.club.json") + }} + end + def get("https://mastodon.social/users/emelie/statuses/101849165031453009", _, _, _) do {:ok, %Tesla.Env{ @@ -340,6 +348,14 @@ def get("http://mastodon.example.org/users/admin", _, _, Accept: "application/ac }} end + def get("http://mastodon.example.org/users/relay", _, _, Accept: "application/activity+json") do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/relay@mastdon.example.org.json") + }} + end + def get("http://mastodon.example.org/users/gargron", _, _, Accept: "application/activity+json") do {:error, :nxdomain} end @@ -620,7 +636,7 @@ def get("https://shitposter.club/notice/2827873", _, _, _) do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.html") + body: File.read!("test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json") }} end @@ -1167,6 +1183,30 @@ def get("https://mstdn.jp/.well-known/webfinger?resource=acct:kpherox@mstdn.jp", }} end + def get("https://10.111.10.1/notice/9kCP7V", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: ""}} + end + + def get("https://172.16.32.40/notice/9kCP7V", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: ""}} + end + + def get("https://192.168.10.40/notice/9kCP7V", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: ""}} + end + + def get("https://www.patreon.com/posts/mastodon-2-9-and-28121681", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: ""}} + end + + def get("http://mastodon.example.org/@admin/99541947525187367", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/mastodon-post-activity.json")}} + end + + def get("https://info.pleroma.site/activity4.json", _, _, _) do + {:ok, %Tesla.Env{status: 500, body: "Error occurred"}} + end + def get("http://example.com/rel_me/anchor", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor.html")}} end @@ -1199,6 +1239,10 @@ def get("https://patch.cx/users/rin", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/rin.json")}} end + def get("http://example.com/rel_me/error", _, _, _) do + {:ok, %Tesla.Env{status: 404, body: ""}} + end + def get(url, query, body, headers) do {:error, "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{ diff --git a/test/tasks/count_statuses_test.exs b/test/tasks/count_statuses_test.exs index 6035da3c3..bb5dc88f8 100644 --- a/test/tasks/count_statuses_test.exs +++ b/test/tasks/count_statuses_test.exs @@ -22,18 +22,18 @@ test "counts statuses" do user = refresh_record(user) user2 = refresh_record(user2) - assert %{info: %{note_count: 2}} = user - assert %{info: %{note_count: 1}} = user2 + assert %{note_count: 2} = user + assert %{note_count: 1} = user2 - {:ok, user} = User.update_info(user, &User.Info.set_note_count(&1, 0)) - {:ok, user2} = User.update_info(user2, &User.Info.set_note_count(&1, 0)) + {:ok, user} = User.update_note_count(user, 0) + {:ok, user2} = User.update_note_count(user2, 0) - assert %{info: %{note_count: 0}} = user - assert %{info: %{note_count: 0}} = user2 + assert %{note_count: 0} = user + assert %{note_count: 0} = user2 assert capture_io(fn -> Mix.Tasks.Pleroma.CountStatuses.run([]) end) == "Done\n" - assert %{info: %{note_count: 2}} = refresh_record(user) - assert %{info: %{note_count: 1}} = refresh_record(user2) + assert %{note_count: 2} = refresh_record(user) + assert %{note_count: 1} = refresh_record(user2) end end diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs index b63dcac00..0c7883f33 100644 --- a/test/tasks/database_test.exs +++ b/test/tasks/database_test.exs @@ -72,26 +72,26 @@ test "it prunes old objects from the database" do describe "running update_users_following_followers_counts" do test "following and followers count are updated" do [user, user2] = insert_pair(:user) - {:ok, %User{following: following, info: info} = user} = User.follow(user, user2) + {:ok, %User{} = user} = User.follow(user, user2) + + following = User.following(user) assert length(following) == 2 - assert info.follower_count == 0 + assert user.follower_count == 0 {:ok, user} = user - |> Ecto.Changeset.change(%{following: following ++ following}) - |> User.change_info(&Ecto.Changeset.change(&1, %{follower_count: 3})) + |> Ecto.Changeset.change(%{follower_count: 3}) |> Repo.update() - assert length(user.following) == 4 - assert user.info.follower_count == 3 + assert user.follower_count == 3 assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"]) user = User.get_by_id(user.id) - assert length(user.following) == 2 - assert user.info.follower_count == 0 + assert length(User.following(user)) == 2 + assert user.follower_count == 0 end end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index c866608ab..04a1e45d7 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -51,7 +51,7 @@ test "relay is unfollowed" do target_user = User.get_cached_by_ap_id(target_instance) follow_activity = Utils.fetch_latest_follow(local_user, target_user) User.follow(local_user, target_user) - assert "#{target_instance}/followers" in refresh_record(local_user).following + assert "#{target_instance}/followers" in User.following(local_user) Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) @@ -68,7 +68,7 @@ test "relay is unfollowed" do assert undo_activity.data["type"] == "Undo" assert undo_activity.data["actor"] == local_user.ap_id assert undo_activity.data["object"] == cancelled_activity.data - refute "#{target_instance}/followers" in refresh_record(local_user).following + refute "#{target_instance}/followers" in User.following(local_user) end end @@ -78,20 +78,18 @@ test "Prints relay subscription list" do refute_receive {:mix_shell, :info, _} - Pleroma.Web.ActivityPub.Relay.get_actor() - |> Ecto.Changeset.change( - following: [ - "http://test-app.com/user/test1", - "http://test-app.com/user/test1", - "http://test-app-42.com/user/test1" - ] - ) - |> Pleroma.User.update_and_set_cache() + relay_user = Relay.get_actor() + + ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] + |> Enum.each(fn ap_id -> + {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) + User.follow(relay_user, user) + end) :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) - assert_receive {:mix_shell, :info, ["test-app.com"]} - assert_receive {:mix_shell, :info, ["test-app-42.com"]} + assert_receive {:mix_shell, :info, ["mstdn.io"]} + assert_receive {:mix_shell, :info, ["mastodon.example.org"]} end end end diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs index cf12d9ed6..bfd0ccbc5 100644 --- a/test/tasks/user_test.exs +++ b/test/tasks/user_test.exs @@ -58,8 +58,8 @@ test "user is created" do assert user.name == unsaved.name assert user.email == unsaved.email assert user.bio == unsaved.bio - assert user.info.is_moderator - assert user.info.is_admin + assert user.is_moderator + assert user.is_admin end test "user is not created" do @@ -113,11 +113,11 @@ test "user is deactivated" do assert message =~ " deactivated" user = User.get_cached_by_nickname(user.nickname) - assert user.info.deactivated + assert user.deactivated end test "user is activated" do - user = insert(:user, info: %{deactivated: true}) + user = insert(:user, deactivated: true) Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname]) @@ -125,7 +125,7 @@ test "user is activated" do assert message =~ " activated" user = User.get_cached_by_nickname(user.nickname) - refute user.info.deactivated + refute user.deactivated end test "no user to toggle" do @@ -139,7 +139,8 @@ test "no user to toggle" do describe "running unsubscribe" do test "user is unsubscribed" do followed = insert(:user) - user = insert(:user, %{following: [User.ap_followers(followed)]}) + user = insert(:user) + User.follow(user, followed, "accept") Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname]) @@ -154,8 +155,8 @@ test "user is unsubscribed" do assert message =~ "Successfully unsubscribed" user = User.get_cached_by_nickname(user.nickname) - assert Enum.empty?(user.following) - assert user.info.deactivated + assert Enum.empty?(User.get_friends(user)) + assert user.deactivated end test "no user to unsubscribe" do @@ -182,13 +183,13 @@ test "All statuses set" do assert message =~ ~r/Admin status .* true/ user = User.get_cached_by_nickname(user.nickname) - assert user.info.is_moderator - assert user.info.locked - assert user.info.is_admin + assert user.is_moderator + assert user.locked + assert user.is_admin end test "All statuses unset" do - user = insert(:user, info: %{is_moderator: true, locked: true, is_admin: true}) + user = insert(:user, locked: true, is_moderator: true, is_admin: true) Mix.Tasks.Pleroma.User.run([ "set", @@ -208,9 +209,9 @@ test "All statuses unset" do assert message =~ ~r/Admin status .* false/ user = User.get_cached_by_nickname(user.nickname) - refute user.info.is_moderator - refute user.info.locked - refute user.info.is_admin + refute user.is_moderator + refute user.locked + refute user.is_admin end test "no user to set status" do @@ -358,28 +359,28 @@ test "it prints an error message when user is not exist" do describe "running toggle_confirmed" do test "user is confirmed" do - %{id: id, nickname: nickname} = insert(:user, info: %{confirmation_pending: false}) + %{id: id, nickname: nickname} = insert(:user, confirmation_pending: false) assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) assert_received {:mix_shell, :info, [message]} assert message == "#{nickname} needs confirmation." user = Repo.get(User, id) - assert user.info.confirmation_pending - assert user.info.confirmation_token + assert user.confirmation_pending + assert user.confirmation_token end test "user is not confirmed" do %{id: id, nickname: nickname} = - insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"}) + insert(:user, confirmation_pending: true, confirmation_token: "some token") assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname]) assert_received {:mix_shell, :info, [message]} assert message == "#{nickname} doesn't need confirmation." user = Repo.get(User, id) - refute user.info.confirmation_pending - refute user.info.confirmation_token + refute user.confirmation_pending + refute user.confirmation_token end test "it prints an error message when user is not exist" do diff --git a/test/user_info_test.exs b/test/user_info_test.exs deleted file mode 100644 index 2d795594e..000000000 --- a/test/user_info_test.exs +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Pleroma.UserInfoTest do - alias Pleroma.Repo - alias Pleroma.User.Info - - use Pleroma.DataCase - - import Pleroma.Factory - - describe "update_email_notifications/2" do - setup do - user = insert(:user, %{info: %{email_notifications: %{"digest" => true}}}) - - {:ok, user: user} - end - - test "Notifications are updated", %{user: user} do - true = user.info.email_notifications["digest"] - changeset = Info.update_email_notifications(user.info, %{"digest" => false}) - assert changeset.valid? - {:ok, result} = Ecto.Changeset.apply_action(changeset, :insert) - assert result.email_notifications["digest"] == false - end - end -end diff --git a/test/user_search_test.exs b/test/user_search_test.exs index f7ab31287..721af1e5b 100644 --- a/test/user_search_test.exs +++ b/test/user_search_test.exs @@ -51,13 +51,6 @@ test "finds a user by full or partial name" do end) end - test "finds users, preferring nickname matches over name matches" do - u1 = insert(:user, %{name: "lain", nickname: "nick1"}) - u2 = insert(:user, %{nickname: "lain", name: "nick1"}) - - assert [u2.id, u1.id] == Enum.map(User.search("lain"), & &1.id) - end - test "finds users, considering density of matched tokens" do u1 = insert(:user, %{name: "Bar Bar plus Word Word"}) u2 = insert(:user, %{name: "Word Word Bar Bar Bar"}) @@ -65,21 +58,6 @@ test "finds users, considering density of matched tokens" do assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id) end - test "finds users, ranking by similarity" do - u1 = insert(:user, %{name: "lain"}) - _u2 = insert(:user, %{name: "ean"}) - u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"}) - u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"}) - - assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple", for_user: u1), & &1.id) - end - - test "finds users, handling misspelled requests" do - u1 = insert(:user, %{name: "lain"}) - - assert [u1.id] == Enum.map(User.search("laiin"), & &1.id) - end - test "finds users, boosting ranks of friends and followers" do u1 = insert(:user) u2 = insert(:user, %{name: "Doe"}) @@ -163,17 +141,6 @@ test "find all users for unauthenticated users when `limit_to_local_content` is Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated) end - test "finds a user whose name is nil" do - _user = insert(:user, %{name: "notamatch", nickname: "testuser@pleroma.amplifie.red"}) - user_two = insert(:user, %{name: nil, nickname: "lain@pleroma.soykaf.com"}) - - assert user_two == - User.search("lain@pleroma.soykaf.com") - |> List.first() - |> Map.put(:search_rank, nil) - |> Map.put(:search_type, nil) - end - test "does not yield false-positive matches" do insert(:user, %{name: "John Doe"}) diff --git a/test/user_test.exs b/test/user_test.exs index 019e7b400..6b1b24ce5 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -68,7 +68,7 @@ test "ap_following returns the following collection for the user" do test "returns all pending follow requests" do unlocked = insert(:user) - locked = insert(:user, %{info: %{locked: true}}) + locked = insert(:user, locked: true) follower = insert(:user) CommonAPI.follow(follower, unlocked) @@ -81,21 +81,20 @@ test "returns all pending follow requests" do end test "doesn't return already accepted or duplicate follow requests" do - locked = insert(:user, %{info: %{locked: true}}) + locked = insert(:user, locked: true) pending_follower = insert(:user) accepted_follower = insert(:user) CommonAPI.follow(pending_follower, locked) CommonAPI.follow(pending_follower, locked) CommonAPI.follow(accepted_follower, locked) - User.follow(accepted_follower, locked) + Pleroma.FollowingRelationship.update(accepted_follower, locked, "accept") - assert [activity] = User.get_follow_requests(locked) - assert activity + assert [^pending_follower] = User.get_follow_requests(locked) end test "clears follow requests when requester is blocked" do - followed = insert(:user, %{info: %{locked: true}}) + followed = insert(:user, locked: true) follower = insert(:user) CommonAPI.follow(follower, followed) @@ -136,10 +135,10 @@ test "follow_all follows mutliple users without duplicating" do followed_two = insert(:user) {:ok, user} = User.follow_all(user, [followed_zero, followed_one]) - assert length(user.following) == 3 + assert length(User.following(user)) == 3 {:ok, user} = User.follow_all(user, [followed_one, followed_two]) - assert length(user.following) == 4 + assert length(User.following(user)) == 4 end test "follow takes a user and another user" do @@ -151,14 +150,14 @@ test "follow takes a user and another user" do user = User.get_cached_by_id(user.id) followed = User.get_cached_by_ap_id(followed.ap_id) - assert followed.info.follower_count == 1 + assert followed.follower_count == 1 - assert User.ap_followers(followed) in user.following + assert User.ap_followers(followed) in User.following(user) end test "can't follow a deactivated users" do user = insert(:user) - followed = insert(:user, info: %{deactivated: true}) + followed = insert(:user, %{deactivated: true}) {:error, _} = User.follow(user, followed) end @@ -182,31 +181,14 @@ test "can't subscribe to a user who blocked us" do end test "local users do not automatically follow local locked accounts" do - follower = insert(:user, info: %{locked: true}) - followed = insert(:user, info: %{locked: true}) + follower = insert(:user, locked: true) + followed = insert(:user, locked: true) {:ok, follower} = User.maybe_direct_follow(follower, followed) refute User.following?(follower, followed) end - # This is a somewhat useless test. - # test "following a remote user will ensure a websub subscription is present" do - # user = insert(:user) - # {:ok, followed} = OStatus.make_user("shp@social.heldscal.la") - - # assert followed.local == false - - # {:ok, user} = User.follow(user, followed) - # assert User.ap_followers(followed) in user.following - - # query = from w in WebsubClientSubscription, - # where: w.topic == ^followed.info["topic"] - # websub = Repo.one(query) - - # assert websub - # end - describe "unfollow/2" do setup do setting = Pleroma.Config.get([:instance, :external_user_synchronization]) @@ -235,26 +217,29 @@ test "unfollow with syncronizes external user" do nickname: "fuser2", ap_id: "http://localhost:4001/users/fuser2", follower_address: "http://localhost:4001/users/fuser2/followers", - following_address: "http://localhost:4001/users/fuser2/following", - following: [User.ap_followers(followed)] + following_address: "http://localhost:4001/users/fuser2/following" }) + {:ok, user} = User.follow(user, followed, "accept") + {:ok, user, _activity} = User.unfollow(user, followed) user = User.get_cached_by_id(user.id) - assert user.following == [] + assert User.following(user) == [] end test "unfollow takes a user and another user" do followed = insert(:user) - user = insert(:user, %{following: [User.ap_followers(followed)]}) + user = insert(:user) + + {:ok, user} = User.follow(user, followed, "accept") + + assert User.following(user) == [user.follower_address, followed.follower_address] {:ok, user, _activity} = User.unfollow(user, followed) - user = User.get_cached_by_id(user.id) - - assert user.following == [] + assert User.following(user) == [user.follower_address] end test "unfollow doesn't unfollow yourself" do @@ -262,14 +247,14 @@ test "unfollow doesn't unfollow yourself" do {:error, _} = User.unfollow(user, user) - user = User.get_cached_by_id(user.id) - assert user.following == [user.ap_id] + assert User.following(user) == [user.follower_address] end end test "test if a user is following another user" do followed = insert(:user) - user = insert(:user, %{following: [User.ap_followers(followed)]}) + user = insert(:user) + User.follow(user, followed, "accept") assert User.following?(user, followed) refute User.following?(followed, user) @@ -352,7 +337,7 @@ test "it restricts certain nicknames" do refute changeset.valid? end - test "it sets the password_hash, ap_id and following fields" do + test "it sets the password_hash and ap_id" do changeset = User.register_changeset(%User{}, @full_user_data) assert changeset.valid? @@ -360,10 +345,6 @@ test "it sets the password_hash, ap_id and following fields" do assert is_binary(changeset.changes[:password_hash]) assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname}) - assert changeset.changes[:following] == [ - User.ap_followers(%User{nickname: @full_user_data.nickname}) - ] - assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" end @@ -400,8 +381,8 @@ test "it creates unconfirmed user" do {:ok, user} = Repo.insert(changeset) - assert user.info.confirmation_pending - assert user.info.confirmation_token + assert user.confirmation_pending + assert user.confirmation_token end test "it creates confirmed user if :confirmed option is given" do @@ -410,8 +391,8 @@ test "it creates confirmed user if :confirmed option is given" do {:ok, user} = Repo.insert(changeset) - refute user.info.confirmation_pending - refute user.info.confirmation_token + refute user.confirmation_pending + refute user.confirmation_token end end @@ -474,11 +455,6 @@ test "gets an existing user by fully qualified nickname, case insensitive" do assert user == fetched_user end - test "fetches an external user via ostatus if no user exists" do - {:ok, fetched_user} = User.get_or_fetch_by_nickname("shp@social.heldscal.la") - assert fetched_user.nickname == "shp@social.heldscal.la" - end - test "returns nil if no user could be fetched" do {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la") assert fetched_user == "not found nonexistant@social.heldscal.la" @@ -505,7 +481,8 @@ test "updates an existing user, if stale" do assert orig_user.last_refreshed_at == a_week_ago {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") - assert user.info.source_data["endpoints"] + + assert user.source_data["endpoints"] refute user.last_refreshed_at == orig_user.last_refreshed_at end @@ -611,94 +588,63 @@ test "gets all friends (followed users) for a given user" do end describe "updating note and follower count" do - test "it sets the info->note_count property" do + test "it sets the note_count property" do note = insert(:note) user = User.get_cached_by_ap_id(note.data["actor"]) - assert user.info.note_count == 0 + assert user.note_count == 0 {:ok, user} = User.update_note_count(user) - assert user.info.note_count == 1 + assert user.note_count == 1 end - test "it increases the info->note_count property" do + test "it increases the note_count property" do note = insert(:note) user = User.get_cached_by_ap_id(note.data["actor"]) - assert user.info.note_count == 0 + assert user.note_count == 0 {:ok, user} = User.increase_note_count(user) - assert user.info.note_count == 1 + assert user.note_count == 1 {:ok, user} = User.increase_note_count(user) - assert user.info.note_count == 2 + assert user.note_count == 2 end - test "it decreases the info->note_count property" do + test "it decreases the note_count property" do note = insert(:note) user = User.get_cached_by_ap_id(note.data["actor"]) - assert user.info.note_count == 0 + assert user.note_count == 0 {:ok, user} = User.increase_note_count(user) - assert user.info.note_count == 1 + assert user.note_count == 1 {:ok, user} = User.decrease_note_count(user) - assert user.info.note_count == 0 + assert user.note_count == 0 {:ok, user} = User.decrease_note_count(user) - assert user.info.note_count == 0 + assert user.note_count == 0 end - test "it sets the info->follower_count property" do + test "it sets the follower_count property" do user = insert(:user) follower = insert(:user) User.follow(follower, user) - assert user.info.follower_count == 0 + assert user.follower_count == 0 {:ok, user} = User.update_follower_count(user) - assert user.info.follower_count == 1 - end - end - - describe "remove duplicates from following list" do - test "it removes duplicates" do - user = insert(:user) - follower = insert(:user) - - {:ok, %User{following: following} = follower} = User.follow(follower, user) - assert length(following) == 2 - - {:ok, follower} = - follower - |> User.update_changeset(%{following: following ++ following}) - |> Repo.update() - - assert length(follower.following) == 4 - - {:ok, follower} = User.remove_duplicated_following(follower) - assert length(follower.following) == 2 - end - - test "it does nothing when following is uniq" do - user = insert(:user) - follower = insert(:user) - - {:ok, follower} = User.follow(follower, user) - assert length(follower.following) == 2 - - {:ok, follower} = User.remove_duplicated_following(follower) - assert length(follower.following) == 2 + assert user.follower_count == 1 end end @@ -932,40 +878,63 @@ test "it imports user blocks from list" do end end - test "get recipients from activity" do - actor = insert(:user) - user = insert(:user, local: true) - user_two = insert(:user, local: false) - addressed = insert(:user, local: true) - addressed_remote = insert(:user, local: false) + describe "get_recipients_from_activity" do + test "get recipients" do + actor = insert(:user) + user = insert(:user, local: true) + user_two = insert(:user, local: false) + addressed = insert(:user, local: true) + addressed_remote = insert(:user, local: false) - {:ok, activity} = - CommonAPI.post(actor, %{ - "status" => "hey @#{addressed.nickname} @#{addressed_remote.nickname}" - }) + {:ok, activity} = + CommonAPI.post(actor, %{ + "status" => "hey @#{addressed.nickname} @#{addressed_remote.nickname}" + }) - assert Enum.map([actor, addressed], & &1.ap_id) -- - Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] + assert Enum.map([actor, addressed], & &1.ap_id) -- + Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] - {:ok, user} = User.follow(user, actor) - {:ok, _user_two} = User.follow(user_two, actor) - recipients = User.get_recipients_from_activity(activity) - assert length(recipients) == 3 - assert user in recipients - assert addressed in recipients + {:ok, user} = User.follow(user, actor) + {:ok, _user_two} = User.follow(user_two, actor) + recipients = User.get_recipients_from_activity(activity) + assert length(recipients) == 3 + assert user in recipients + assert addressed in recipients + end + + test "has following" do + actor = insert(:user) + user = insert(:user) + user_two = insert(:user) + addressed = insert(:user, local: true) + + {:ok, activity} = + CommonAPI.post(actor, %{ + "status" => "hey @#{addressed.nickname}" + }) + + assert Enum.map([actor, addressed], & &1.ap_id) -- + Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] + + {:ok, _actor} = User.follow(actor, user) + {:ok, _actor} = User.follow(actor, user_two) + recipients = User.get_recipients_from_activity(activity) + assert length(recipients) == 2 + assert addressed in recipients + end end describe ".deactivate" do test "can de-activate then re-activate a user" do user = insert(:user) - assert false == user.info.deactivated + assert false == user.deactivated {:ok, user} = User.deactivate(user) - assert true == user.info.deactivated + assert true == user.deactivated {:ok, user} = User.deactivate(user, false) - assert false == user.info.deactivated + assert false == user.deactivated end - test "hide a user from followers " do + test "hide a user from followers" do user = insert(:user) user2 = insert(:user) @@ -1010,7 +979,9 @@ test "hide a user's statuses from timelines and notifications" do assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark) assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] == - ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2}) + ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ + "user" => user2 + }) {:ok, _user} = User.deactivate(user) @@ -1018,7 +989,9 @@ test "hide a user's statuses from timelines and notifications" do assert [] == Pleroma.Notification.for_user(user2) assert [] == - ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2}) + ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{ + "user" => user2 + }) end end @@ -1041,7 +1014,7 @@ test ".delete_user_activities deletes all create activities", %{user: user} do end test "it deletes deactivated user" do - {:ok, user} = insert(:user, info: %{deactivated: true}) |> User.set_cache() + {:ok, user} = insert(:user, deactivated: true) |> User.set_cache() {:ok, job} = User.delete(user) {:ok, _user} = ObanHelpers.perform(job) @@ -1132,11 +1105,9 @@ test "with overly long fields" do ap_id: user.ap_id, name: user.name, nickname: user.nickname, - info: %{ - fields: [ - %{"name" => "myfield", "value" => String.duplicate("h", current_max_length + 1)} - ] - } + fields: [ + %{"name" => "myfield", "value" => String.duplicate("h", current_max_length + 1)} + ] } assert {:ok, %User{}} = User.insert_or_update_user(data) @@ -1180,7 +1151,7 @@ test "html_filter_policy returns default policies, when rich-text is enabled" do end test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do - user = insert(:user, %{info: %{no_rich_text: true}}) + user = insert(:user, no_rich_text: true) assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user) end @@ -1217,8 +1188,8 @@ test "User.delete() plugs any possible zombie objects" do test "auth_active?/1 works correctly" do Pleroma.Config.put([:instance, :account_activation_required], true) - local_user = insert(:user, local: true, info: %{confirmation_pending: true}) - confirmed_user = insert(:user, local: true, info: %{confirmation_pending: false}) + local_user = insert(:user, local: true, confirmation_pending: true) + confirmed_user = insert(:user, local: true, confirmation_pending: false) remote_user = insert(:user, local: false) refute User.auth_active?(local_user) @@ -1235,25 +1206,39 @@ test "returns false for unprivileged users" do test "returns false for remote users" do user = insert(:user, local: false) - remote_admin_user = insert(:user, local: false, info: %{is_admin: true}) + remote_admin_user = insert(:user, local: false, is_admin: true) refute User.superuser?(user) refute User.superuser?(remote_admin_user) end test "returns true for local moderators" do - user = insert(:user, local: true, info: %{is_moderator: true}) + user = insert(:user, local: true, is_moderator: true) assert User.superuser?(user) end test "returns true for local admins" do - user = insert(:user, local: true, info: %{is_admin: true}) + user = insert(:user, local: true, is_admin: true) assert User.superuser?(user) end end + describe "invisible?/1" do + test "returns true for an invisible user" do + user = insert(:user, local: true, invisible: true) + + assert User.invisible?(user) + end + + test "returns false for a non-invisible user" do + user = insert(:user, local: true) + + refute User.invisible?(user) + end + end + describe "visible_for?/2" do test "returns true when the account is itself" do user = insert(:user, local: true) @@ -1264,14 +1249,14 @@ test "returns true when the account is itself" do test "returns false when the account is unauthenticated and auth is required" do Pleroma.Config.put([:instance, :account_activation_required], true) - user = insert(:user, local: true, info: %{confirmation_pending: true}) + user = insert(:user, local: true, confirmation_pending: true) other_user = insert(:user, local: true) refute User.visible_for?(user, other_user) end test "returns true when the account is unauthenticated and auth is not required" do - user = insert(:user, local: true, info: %{confirmation_pending: true}) + user = insert(:user, local: true, confirmation_pending: true) other_user = insert(:user, local: true) assert User.visible_for?(user, other_user) @@ -1280,8 +1265,8 @@ test "returns true when the account is unauthenticated and auth is not required" test "returns true when the account is unauthenticated and being viewed by a privileged account (auth required)" do Pleroma.Config.put([:instance, :account_activation_required], true) - user = insert(:user, local: true, info: %{confirmation_pending: true}) - other_user = insert(:user, local: true, info: %{is_admin: true}) + user = insert(:user, local: true, confirmation_pending: true) + other_user = insert(:user, local: true, is_admin: true) assert User.visible_for?(user, other_user) end @@ -1347,7 +1332,7 @@ test "Users are inactive by default" do users = Enum.map(1..total, fn _ -> - insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false}) + insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) end) inactive_users_ids = @@ -1365,7 +1350,7 @@ test "Only includes users who has no recent activity" do users = Enum.map(1..total, fn _ -> - insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false}) + insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) end) {inactive, active} = Enum.split(users, trunc(total / 2)) @@ -1398,7 +1383,7 @@ test "Only includes users with no read notifications" do users = Enum.map(1..total, fn _ -> - insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false}) + insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false) end) [sender | recipients] = users @@ -1438,19 +1423,19 @@ test "Only includes users with no read notifications" do describe "toggle_confirmation/1" do test "if user is confirmed" do - user = insert(:user, info: %{confirmation_pending: false}) + user = insert(:user, confirmation_pending: false) {:ok, user} = User.toggle_confirmation(user) - assert user.info.confirmation_pending - assert user.info.confirmation_token + assert user.confirmation_pending + assert user.confirmation_token end test "if user is unconfirmed" do - user = insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"}) + user = insert(:user, confirmation_pending: true, confirmation_token: "some token") {:ok, user} = User.toggle_confirmation(user) - refute user.info.confirmation_pending - refute user.info.confirmation_token + refute user.confirmation_pending + refute user.confirmation_token end end @@ -1486,7 +1471,7 @@ test "it returns a list of AP ids for a given set of nicknames" do user1 = insert(:user, local: false, ap_id: "http://localhost:4001/users/masto_closed") user2 = insert(:user, local: false, ap_id: "http://localhost:4001/users/fuser2") insert(:user, local: true) - insert(:user, local: false, info: %{deactivated: true}) + insert(:user, local: false, deactivated: true) {:ok, user1: user1, user2: user2} end @@ -1605,7 +1590,7 @@ test "updates the counters normally on following/getting a follow when disabled" local: false, follower_address: "http://localhost:4001/users/masto_closed/followers", following_address: "http://localhost:4001/users/masto_closed/following", - info: %{ap_enabled: true} + ap_enabled: true ) assert User.user_info(other_user).following_count == 0 @@ -1628,7 +1613,7 @@ test "syncronizes the counters with the remote instance for the followed when en local: false, follower_address: "http://localhost:4001/users/masto_closed/followers", following_address: "http://localhost:4001/users/masto_closed/following", - info: %{ap_enabled: true} + ap_enabled: true ) assert User.user_info(other_user).following_count == 0 @@ -1651,7 +1636,7 @@ test "syncronizes the counters with the remote instance for the follower when en local: false, follower_address: "http://localhost:4001/users/masto_closed/followers", following_address: "http://localhost:4001/users/masto_closed/following", - info: %{ap_enabled: true} + ap_enabled: true ) assert User.user_info(other_user).following_count == 0 @@ -1691,41 +1676,6 @@ test "changes email", %{user: user} do end end - describe "set_password_reset_pending/2" do - setup do - [user: insert(:user)] - end - - test "sets password_reset_pending to true", %{user: user} do - %{password_reset_pending: password_reset_pending} = user.info - - refute password_reset_pending - - {:ok, %{info: %{password_reset_pending: password_reset_pending}}} = - User.force_password_reset(user) - - assert password_reset_pending - end - end - - test "change_info/2" do - user = insert(:user) - assert user.info.hide_follows == false - - changeset = User.change_info(user, &User.Info.profile_update(&1, %{hide_follows: true})) - assert changeset.changes.info.changes.hide_follows == true - end - - test "update_info/2" do - user = insert(:user) - assert user.info.hide_follows == false - - assert {:ok, _} = User.update_info(user, &User.Info.profile_update(&1, %{hide_follows: true})) - - assert %{info: %{hide_follows: true}} = Repo.get(User, user.id) - assert {:ok, %{info: %{hide_follows: true}}} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") - end - describe "get_cached_by_nickname_or_id" do setup do limit_to_local_content = Pleroma.Config.get([:instance, :limit_to_local_content]) @@ -1782,4 +1732,18 @@ test "allows getting local users by nickname no matter what :limit_to_local_cont assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname) end end + + describe "update_email_notifications/2" do + setup do + user = insert(:user, email_notifications: %{"digest" => true}) + + {:ok, user: user} + end + + test "Notifications are updated", %{user: user} do + true = user.email_notifications["digest"] + assert {:ok, result} = User.update_email_notifications(user, %{"digest" => false}) + assert result.email_notifications["digest"] == false + end + end end diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 6a3e48b5e..a5414c521 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -354,6 +354,87 @@ test "it inserts an incoming activity into the database", %{conn: conn, data: da assert Activity.get_by_ap_id(data["id"]) end + test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do + user = insert(:user) + + data = + Map.put(data, "to", user.ap_id) + |> Map.delete("cc") + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do + user = insert(:user) + + data = + Map.put(data, "cc", user.ap_id) + |> Map.delete("to") + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + %Activity{} = activity = Activity.get_by_ap_id(data["id"]) + assert user.ap_id in activity.recipients + end + + test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do + user = insert(:user) + + data = + Map.put(data, "bcc", user.ap_id) + |> Map.delete("to") + |> Map.delete("cc") + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it accepts announces with to as string instead of array", %{conn: conn} do + user = insert(:user) + + data = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => "http://mastodon.example.org/users/admin", + "id" => "http://mastodon.example.org/users/admin/statuses/19512778738411822/activity", + "object" => "https://mastodon.social/users/emelie/statuses/101849165031453009", + "to" => "https://www.w3.org/ns/activitystreams#Public", + "cc" => [user.ap_id], + "type" => "Announce" + } + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) + %Activity{} = activity = Activity.get_by_ap_id(data["id"]) + assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients + end + test "it accepts messages from actors that are followed by the user", %{ conn: conn, data: data @@ -683,7 +764,7 @@ test "it returns the followers in a collection", %{conn: conn} do test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do user = insert(:user) - user_two = insert(:user, %{info: %{hide_followers: true}}) + user_two = insert(:user, hide_followers: true) User.follow(user, user_two) result = @@ -696,7 +777,7 @@ test "it returns returns a uri if the user has 'hide_followers' set", %{conn: co test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated", %{conn: conn} do - user = insert(:user, %{info: %{hide_followers: true}}) + user = insert(:user, hide_followers: true) result = conn @@ -708,7 +789,7 @@ test "it returns a 403 error on pages, if the user has 'hide_followers' set and test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user", %{conn: conn} do - user = insert(:user, %{info: %{hide_followers: true}}) + user = insert(:user, hide_followers: true) other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) @@ -764,7 +845,7 @@ test "it returns the following in a collection", %{conn: conn} do end test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do - user = insert(:user, %{info: %{hide_follows: true}}) + user = insert(:user, hide_follows: true) user_two = insert(:user) User.follow(user, user_two) @@ -778,7 +859,7 @@ test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated", %{conn: conn} do - user = insert(:user, %{info: %{hide_follows: true}}) + user = insert(:user, hide_follows: true) result = conn @@ -790,7 +871,7 @@ test "it returns a 403 error on pages, if the user has 'hide_follows' set and th test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user", %{conn: conn} do - user = insert(:user, %{info: %{hide_follows: true}}) + user = insert(:user, hide_follows: true) other_user = insert(:user) {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index b0a008671..a7d5eabf9 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.CommonAPI import Pleroma.Factory @@ -41,6 +42,27 @@ test "it streams them out" do assert called(Pleroma.Web.Streamer.stream("participation", participations)) end end + + test "streams them out on activity creation" do + user_one = insert(:user) + user_two = insert(:user) + + with_mock Pleroma.Web.Streamer, + stream: fn _, _ -> nil end do + {:ok, activity} = + CommonAPI.post(user_one, %{ + "status" => "@#{user_two.nickname}", + "visibility" => "direct" + }) + + conversation = + activity.data["context"] + |> Pleroma.Conversation.get_for_ap_id() + |> Repo.preload(participations: :user) + + assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations)) + end + end end describe "fetching restricted by visibility" do @@ -87,17 +109,83 @@ test "it restricts by the appropriate visibility" do end end + describe "fetching excluded by visibility" do + test "it excludes by the appropriate visibility" do + user = insert(:user) + + {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"}) + + {:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) + + {:ok, unlisted_activity} = + CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"}) + + {:ok, private_activity} = + CommonAPI.post(user, %{"status" => ".", "visibility" => "private"}) + + activities = + ActivityPub.fetch_activities([], %{ + "exclude_visibilities" => "direct", + "actor_id" => user.ap_id + }) + + assert public_activity in activities + assert unlisted_activity in activities + assert private_activity in activities + refute direct_activity in activities + + activities = + ActivityPub.fetch_activities([], %{ + "exclude_visibilities" => "unlisted", + "actor_id" => user.ap_id + }) + + assert public_activity in activities + refute unlisted_activity in activities + assert private_activity in activities + assert direct_activity in activities + + activities = + ActivityPub.fetch_activities([], %{ + "exclude_visibilities" => "private", + "actor_id" => user.ap_id + }) + + assert public_activity in activities + assert unlisted_activity in activities + refute private_activity in activities + assert direct_activity in activities + + activities = + ActivityPub.fetch_activities([], %{ + "exclude_visibilities" => "public", + "actor_id" => user.ap_id + }) + + refute public_activity in activities + assert unlisted_activity in activities + assert private_activity in activities + assert direct_activity in activities + end + end + describe "building a user from his ap id" do test "it returns a user" do user_id = "http://mastodon.example.org/users/admin" {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) assert user.ap_id == user_id assert user.nickname == "admin@mastodon.example.org" - assert user.info.source_data - assert user.info.ap_enabled + assert user.source_data + assert user.ap_enabled assert user.follower_address == "http://mastodon.example.org/users/admin/followers" end + test "it returns a user that is invisible" do + user_id = "http://mastodon.example.org/users/relay" + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) + assert User.invisible?(user) + end + test "it fetches the appropriate tag-restricted posts" do user = insert(:user) @@ -279,7 +367,7 @@ test "does not increase user note count" do assert activity.actor == user.ap_id user = User.get_cached_by_id(user.id) - assert user.info.note_count == 0 + assert user.note_count == 0 end test "can be fetched into a timeline" do @@ -342,7 +430,7 @@ test "increases user note count only for public activities" do }) user = User.get_cached_by_id(user.id) - assert user.info.note_count == 2 + assert user.note_count == 2 end test "increases replies count" do @@ -606,7 +694,7 @@ test "does include announces on request" do {:ok, announce, _object} = CommonAPI.repeat(activity_three.id, booster) - [announce_activity] = ActivityPub.fetch_activities([user.ap_id | user.following]) + [announce_activity] = ActivityPub.fetch_activities([user.ap_id | User.following(user)]) assert announce_activity.id == announce.id end @@ -1081,7 +1169,7 @@ test "it creates a delete activity and deletes the original object" do end test "decrements user note count only for public activities" do - user = insert(:user, info: %{note_count: 10}) + user = insert(:user, note_count: 10) {:ok, a1} = CommonAPI.post(User.get_cached_by_id(user.id), %{ @@ -1113,7 +1201,7 @@ test "decrements user note count only for public activities" do {:ok, _} = Object.normalize(a4) |> ActivityPub.delete() user = User.get_cached_by_id(user.id) - assert user.info.note_count == 10 + assert user.note_count == 10 end test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do @@ -1204,7 +1292,7 @@ test "it filters broken threads" do }) activities = - ActivityPub.fetch_activities([user1.ap_id | user1.following]) + ActivityPub.fetch_activities([user1.ap_id | User.following(user1)]) |> Enum.map(fn a -> a.id end) private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"]) @@ -1214,7 +1302,7 @@ test "it filters broken threads" do assert length(activities) == 3 activities = - ActivityPub.fetch_activities([user1.ap_id | user1.following], %{"user" => user1}) + ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{"user" => user1}) |> Enum.map(fn a -> a.id end) assert [public_activity.id, private_activity_1.id] == activities @@ -1266,35 +1354,99 @@ test "returned pinned statuses" do assert 3 = length(activities) end - test "it can create a Flag activity" do - reporter = insert(:user) - target_account = insert(:user) - {:ok, activity} = CommonAPI.post(target_account, %{"status" => "foobar"}) - context = Utils.generate_context_id() - content = "foobar" + describe "flag/1" do + setup do + reporter = insert(:user) + target_account = insert(:user) + content = "foobar" + {:ok, activity} = CommonAPI.post(target_account, %{"status" => content}) + context = Utils.generate_context_id() - reporter_ap_id = reporter.ap_id - target_ap_id = target_account.ap_id - activity_ap_id = activity.data["id"] + reporter_ap_id = reporter.ap_id + target_ap_id = target_account.ap_id + activity_ap_id = activity.data["id"] - assert {:ok, activity} = - ActivityPub.flag(%{ - actor: reporter, - context: context, - account: target_account, - statuses: [activity], - content: content - }) + activity_with_object = Activity.get_by_ap_id_with_object(activity_ap_id) - assert %Activity{ - actor: ^reporter_ap_id, - data: %{ - "type" => "Flag", - "content" => ^content, - "context" => ^context, - "object" => [^target_ap_id, ^activity_ap_id] - } - } = activity + {:ok, + %{ + reporter: reporter, + context: context, + target_account: target_account, + reported_activity: activity, + content: content, + activity_ap_id: activity_ap_id, + activity_with_object: activity_with_object, + reporter_ap_id: reporter_ap_id, + target_ap_id: target_ap_id + }} + end + + test "it can create a Flag activity", + %{ + reporter: reporter, + context: context, + target_account: target_account, + reported_activity: reported_activity, + content: content, + activity_ap_id: activity_ap_id, + activity_with_object: activity_with_object, + reporter_ap_id: reporter_ap_id, + target_ap_id: target_ap_id + } do + assert {:ok, activity} = + ActivityPub.flag(%{ + actor: reporter, + context: context, + account: target_account, + statuses: [reported_activity], + content: content + }) + + note_obj = %{ + "type" => "Note", + "id" => activity_ap_id, + "content" => content, + "published" => activity_with_object.object.data["published"], + "actor" => AccountView.render("show.json", %{user: target_account}) + } + + assert %Activity{ + actor: ^reporter_ap_id, + data: %{ + "type" => "Flag", + "content" => ^content, + "context" => ^context, + "object" => [^target_ap_id, ^note_obj] + } + } = activity + end + + test_with_mock "strips status data from Flag, before federating it", + %{ + reporter: reporter, + context: context, + target_account: target_account, + reported_activity: reported_activity, + content: content + }, + Utils, + [:passthrough], + [] do + {:ok, activity} = + ActivityPub.flag(%{ + actor: reporter, + context: context, + account: target_account, + statuses: [reported_activity], + content: content + }) + + new_data = + put_in(activity.data, ["object"], [target_account.ap_id, reported_activity.data["id"]]) + + assert_called(Utils.maybe_federate(%{activity | data: new_data})) + end end test "fetch_activities/2 returns activities addressed to a list " do @@ -1377,9 +1529,9 @@ test "detects hidden followers" do following_address: "http://localhost:4001/users/masto_closed/following" ) - {:ok, info} = ActivityPub.fetch_follow_information_for_user(user) - assert info.hide_followers == true - assert info.hide_follows == false + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + assert follow_info.hide_followers == true + assert follow_info.hide_follows == false end test "detects hidden follows" do @@ -1400,9 +1552,9 @@ test "detects hidden follows" do following_address: "http://localhost:4001/users/masto_closed/following" ) - {:ok, info} = ActivityPub.fetch_follow_information_for_user(user) - assert info.hide_followers == false - assert info.hide_follows == true + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + assert follow_info.hide_followers == false + assert follow_info.hide_follows == true end end end diff --git a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs index 03dc299ec..b524fdd23 100644 --- a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs +++ b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs @@ -35,7 +35,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do test "it allows posts without links" do user = insert(:user) - assert user.info.note_count == 0 + assert user.note_count == 0 message = @linkless_message @@ -47,7 +47,7 @@ test "it allows posts without links" do test "it disallows posts with links" do user = insert(:user) - assert user.info.note_count == 0 + assert user.note_count == 0 message = @linkful_message @@ -59,9 +59,9 @@ test "it disallows posts with links" do describe "with old user" do test "it allows posts without links" do - user = insert(:user, info: %{note_count: 1}) + user = insert(:user, note_count: 1) - assert user.info.note_count == 1 + assert user.note_count == 1 message = @linkless_message @@ -71,9 +71,9 @@ test "it allows posts without links" do end test "it allows posts with links" do - user = insert(:user, info: %{note_count: 1}) + user = insert(:user, note_count: 1) - assert user.info.note_count == 1 + assert user.note_count == 1 message = @linkful_message @@ -85,9 +85,9 @@ test "it allows posts with links" do describe "with followed new user" do test "it allows posts without links" do - user = insert(:user, info: %{follower_count: 1}) + user = insert(:user, follower_count: 1) - assert user.info.follower_count == 1 + assert user.follower_count == 1 message = @linkless_message @@ -97,9 +97,9 @@ test "it allows posts without links" do end test "it allows posts with links" do - user = insert(:user, info: %{follower_count: 1}) + user = insert(:user, follower_count: 1) - assert user.info.follower_count == 1 + assert user.follower_count == 1 message = @linkful_message @@ -133,7 +133,7 @@ test "it rejects posts with links" do describe "with contentless-objects" do test "it does not reject them or error out" do - user = insert(:user, info: %{note_count: 1}) + user = insert(:user, note_count: 1) message = @response_message diff --git a/test/web/activity_pub/mrf/normalize_markup_test.exs b/test/web/activity_pub/mrf/normalize_markup_test.exs index 3916a1f35..0207be56b 100644 --- a/test/web/activity_pub/mrf/normalize_markup_test.exs +++ b/test/web/activity_pub/mrf/normalize_markup_test.exs @@ -20,11 +20,11 @@ test "it filter html tags" do expected = """ this is in bold

this is a paragraph

- this is a linebreak
- this is a link with allowed "rel" attribute: - this is a link with not allowed "rel" attribute: example.com - this is an image:
- alert('hacked') + this is a linebreak
+ this is a link with allowed "rel" attribute: + this is a link with not allowed "rel" attribute: example.com + this is an image:
+ alert('hacked') """ message = %{"type" => "Create", "object" => %{"content" => @html_sample}} diff --git a/test/web/activity_pub/publisher_test.exs b/test/web/activity_pub/publisher_test.exs index df03b4008..e885e5a5a 100644 --- a/test/web/activity_pub/publisher_test.exs +++ b/test/web/activity_pub/publisher_test.exs @@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do test "it returns sharedInbox for messages involving as:Public in to" do user = insert(:user, %{ - info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}} + source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}} }) activity = %Activity{ @@ -40,7 +40,7 @@ test "it returns sharedInbox for messages involving as:Public in to" do test "it returns sharedInbox for messages involving as:Public in cc" do user = insert(:user, %{ - info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}} + source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}} }) activity = %Activity{ @@ -53,7 +53,7 @@ test "it returns sharedInbox for messages involving as:Public in cc" do test "it returns sharedInbox for messages involving multiple recipients in to" do user = insert(:user, %{ - info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}} + source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}} }) user_two = insert(:user) @@ -69,7 +69,7 @@ test "it returns sharedInbox for messages involving multiple recipients in to" d test "it returns sharedInbox for messages involving multiple recipients in cc" do user = insert(:user, %{ - info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}} + source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}} }) user_two = insert(:user) @@ -84,14 +84,12 @@ test "it returns sharedInbox for messages involving multiple recipients in cc" d test "it returns sharedInbox for messages involving multiple recipients in total" do user = - insert(:user, %{ - info: %{ - source_data: %{ - "inbox" => "http://example.com/personal-inbox", - "endpoints" => %{"sharedInbox" => "http://example.com/inbox"} - } + insert(:user, + source_data: %{ + "inbox" => "http://example.com/personal-inbox", + "endpoints" => %{"sharedInbox" => "http://example.com/inbox"} } - }) + ) user_two = insert(:user) @@ -104,14 +102,12 @@ test "it returns sharedInbox for messages involving multiple recipients in total test "it returns inbox for messages involving single recipients in total" do user = - insert(:user, %{ - info: %{ - source_data: %{ - "inbox" => "http://example.com/personal-inbox", - "endpoints" => %{"sharedInbox" => "http://example.com/inbox"} - } + insert(:user, + source_data: %{ + "inbox" => "http://example.com/personal-inbox", + "endpoints" => %{"sharedInbox" => "http://example.com/inbox"} } - }) + ) activity = %Activity{ data: %{"to" => [user.ap_id], "cc" => []} @@ -241,10 +237,8 @@ test "it returns inbox for messages involving single recipients in total" do follower = insert(:user, local: false, - info: %{ - ap_enabled: true, - source_data: %{"inbox" => "https://domain.com/users/nick1/inbox"} - } + source_data: %{"inbox" => "https://domain.com/users/nick1/inbox"}, + ap_enabled: true ) actor = insert(:user, follower_address: follower.ap_id) @@ -278,19 +272,15 @@ test "it returns inbox for messages involving single recipients in total" do fetcher = insert(:user, local: false, - info: %{ - ap_enabled: true, - source_data: %{"inbox" => "https://domain.com/users/nick1/inbox"} - } + source_data: %{"inbox" => "https://domain.com/users/nick1/inbox"}, + ap_enabled: true ) another_fetcher = insert(:user, local: false, - info: %{ - ap_enabled: true, - source_data: %{"inbox" => "https://domain2.com/users/nick1/inbox"} - } + source_data: %{"inbox" => "https://domain2.com/users/nick1/inbox"}, + ap_enabled: true ) actor = insert(:user) diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs index 0f7556538..98dc78f46 100644 --- a/test/web/activity_pub/relay_test.exs +++ b/test/web/activity_pub/relay_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do alias Pleroma.Activity alias Pleroma.Object + alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Relay @@ -19,11 +20,16 @@ test "gets an actor for the relay" do assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay" end + test "relay actor is invisible" do + user = Relay.get_actor() + assert User.invisible?(user) + end + describe "follow/1" do test "returns errors when user not found" do assert capture_log(fn -> - assert Relay.follow("test-ap-id") == {:error, "Could not fetch by AP id"} - end) =~ "Could not fetch by AP id" + {:error, _} = Relay.follow("test-ap-id") + end) =~ "Could not decode user at fetch" end test "returns activity" do @@ -41,8 +47,8 @@ test "returns activity" do describe "unfollow/1" do test "returns errors when user not found" do assert capture_log(fn -> - assert Relay.unfollow("test-ap-id") == {:error, "Could not fetch by AP id"} - end) =~ "Could not fetch by AP id" + {:error, _} = Relay.unfollow("test-ap-id") + end) =~ "Could not decode user at fetch" end test "returns activity" do @@ -50,14 +56,14 @@ test "returns activity" do service_actor = Relay.get_actor() ActivityPub.follow(service_actor, user) Pleroma.User.follow(service_actor, user) - assert "#{user.ap_id}/followers" in refresh_record(service_actor).following + assert "#{user.ap_id}/followers" in User.following(service_actor) assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id) assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" assert user.ap_id in activity.recipients assert activity.data["type"] == "Undo" assert activity.data["actor"] == service_actor.ap_id assert activity.data["to"] == [user.ap_id] - refute "#{user.ap_id}/followers" in refresh_record(service_actor).following + refute "#{user.ap_id}/followers" in User.following(service_actor) 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 99ab573c5..75cfbea2e 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -58,7 +58,7 @@ test "it works for incoming follow requests" do end test "with locked accounts, it does not create a follow or an accept" do - user = insert(:user, info: %{locked: true}) + user = insert(:user, locked: true) 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 0bff5a57b..0bdd514e9 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -7,14 +7,12 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do alias Pleroma.Activity alias Pleroma.Object alias Pleroma.Object.Fetcher - alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OStatus - alias Pleroma.Web.Websub.WebsubClientSubscription import Mock import Pleroma.Factory @@ -148,7 +146,7 @@ test "it works for incoming notices" do user = User.get_cached_by_ap_id(object_data["actor"]) - assert user.info.note_count == 1 + assert user.note_count == 1 end test "it works for incoming notices with hashtags" do @@ -673,7 +671,7 @@ test "it works for incoming update activities" do } ] - assert user.info.banner["url"] == [ + assert user.banner["url"] == [ %{ "href" => "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" @@ -692,7 +690,7 @@ test "it works with custom profile fields" do user = User.get_cached_by_ap_id(activity.actor) - assert User.Info.fields(user.info) == [ + assert User.fields(user) == [ %{"name" => "foo", "value" => "bar"}, %{"name" => "foo1", "value" => "bar1"} ] @@ -713,7 +711,7 @@ test "it works with custom profile fields" do user = User.get_cached_by_ap_id(user.ap_id) - assert User.Info.fields(user.info) == [ + assert User.fields(user) == [ %{"name" => "foo", "value" => "updated"}, %{"name" => "foo1", "value" => "updated"} ] @@ -731,7 +729,7 @@ test "it works with custom profile fields" do user = User.get_cached_by_ap_id(user.ap_id) - assert User.Info.fields(user.info) == [ + assert User.fields(user) == [ %{"name" => "foo", "value" => "updated"}, %{"name" => "foo1", "value" => "updated"} ] @@ -742,7 +740,7 @@ test "it works with custom profile fields" do user = User.get_cached_by_ap_id(user.ap_id) - assert User.Info.fields(user.info) == [] + assert User.fields(user) == [] end test "it works for incoming update activities which lock the account" do @@ -765,11 +763,12 @@ test "it works for incoming update activities which lock the account" do {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data) user = User.get_cached_by_ap_id(data["actor"]) - assert user.info.locked == true + assert user.locked == true end test "it works for incoming deletes" do activity = insert(:note_activity) + deleting_user = insert(:user) data = File.read!("test/fixtures/mastodon-delete.json") @@ -782,11 +781,14 @@ test "it works for incoming deletes" do data = data |> Map.put("object", object) - |> Map.put("actor", activity.data["actor"]) + |> Map.put("actor", deleting_user.ap_id) - {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) + {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} = + Transmogrifier.handle_incoming(data) + assert id == data["id"] refute Activity.get_by_id(activity.id) + assert actor == deleting_user.ap_id end test "it fails for incoming deletes with spoofed origin" do @@ -807,7 +809,7 @@ test "it fails for incoming deletes with spoofed origin" do assert capture_log(fn -> :error = Transmogrifier.handle_incoming(data) end) =~ - "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, {:error, :nxdomain}}" + "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, :nxdomain}" assert Activity.get_by_id(activity.id) end @@ -833,7 +835,10 @@ test "it fails for incoming user deletes with spoofed origin" do |> Poison.decode!() |> Map.put("actor", ap_id) - assert :error == Transmogrifier.handle_incoming(data) + assert capture_log(fn -> + assert :error == Transmogrifier.handle_incoming(data) + end) =~ "Object containment failed" + assert User.get_cached_by_ap_id(ap_id) end @@ -891,6 +896,25 @@ test "it works for incomming unfollows with an existing follow" do refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) end + test "it works for incoming follows to locked account" do + pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin") + user = insert(:user, locked: true) + + data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Follow" + assert data["object"] == user.ap_id + assert data["state"] == "pending" + assert data["actor"] == "http://mastodon.example.org/users/admin" + + assert [^pending_follower] = User.get_follow_requests(user) + end + test "it works for incoming blocks" do user = insert(:user) @@ -993,6 +1017,8 @@ test "it works for incoming accepts which were pre-accepted" do assert activity.data["object"] == follow_activity.data["id"] + assert activity.data["id"] == accept_data["id"] + follower = User.get_cached_by_id(follower.id) assert User.following?(follower, followed) == true @@ -1000,7 +1026,7 @@ test "it works for incoming accepts which were pre-accepted" do test "it works for incoming accepts which were orphaned" do follower = insert(:user) - followed = insert(:user, %{info: %User.Info{locked: true}}) + followed = insert(:user, locked: true) {:ok, follow_activity} = ActivityPub.follow(follower, followed) @@ -1022,7 +1048,7 @@ test "it works for incoming accepts which were orphaned" do test "it works for incoming accepts which are referenced by IRI only" do follower = insert(:user) - followed = insert(:user, %{info: %User.Info{locked: true}}) + followed = insert(:user, locked: true) {:ok, follow_activity} = ActivityPub.follow(follower, followed) @@ -1042,7 +1068,7 @@ test "it works for incoming accepts which are referenced by IRI only" do test "it fails for incoming accepts which cannot be correlated" do follower = insert(:user) - followed = insert(:user, %{info: %User.Info{locked: true}}) + followed = insert(:user, locked: true) accept_data = File.read!("test/fixtures/mastodon-accept-activity.json") @@ -1061,7 +1087,7 @@ test "it fails for incoming accepts which cannot be correlated" do test "it fails for incoming rejects which cannot be correlated" do follower = insert(:user) - followed = insert(:user, %{info: %User.Info{locked: true}}) + followed = insert(:user, locked: true) accept_data = File.read!("test/fixtures/mastodon-reject-activity.json") @@ -1080,7 +1106,7 @@ test "it fails for incoming rejects which cannot be correlated" do test "it works for incoming rejects which are orphaned" do follower = insert(:user) - followed = insert(:user, %{info: %User.Info{locked: true}}) + followed = insert(:user, locked: true) {:ok, follower} = User.follow(follower, followed) {:ok, _follow_activity} = ActivityPub.follow(follower, followed) @@ -1097,6 +1123,7 @@ test "it works for incoming rejects which are orphaned" do {:ok, activity} = Transmogrifier.handle_incoming(reject_data) refute activity.local + assert activity.data["id"] == reject_data["id"] follower = User.get_cached_by_id(follower.id) @@ -1105,7 +1132,7 @@ test "it works for incoming rejects which are orphaned" do test "it works for incoming rejects which are referenced by IRI only" do follower = insert(:user) - followed = insert(:user, %{info: %User.Info{locked: true}}) + followed = insert(:user, locked: true) {:ok, follower} = User.follow(follower, followed) {:ok, follow_activity} = ActivityPub.follow(follower, followed) @@ -1174,10 +1201,18 @@ test "it accepts Flag activities" do {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"}) object = Object.normalize(activity) + note_obj = %{ + "type" => "Note", + "id" => activity.data["id"], + "content" => "test post", + "published" => object.data["published"], + "actor" => AccountView.render("show.json", %{user: user}) + } + message = %{ "@context" => "https://www.w3.org/ns/activitystreams", "cc" => [user.ap_id], - "object" => [user.ap_id, object.data["id"]], + "object" => [user.ap_id, activity.data["id"]], "type" => "Flag", "content" => "blocked AND reported!!!", "actor" => other_user.ap_id @@ -1185,11 +1220,55 @@ test "it accepts Flag activities" do assert {:ok, activity} = Transmogrifier.handle_incoming(message) - assert activity.data["object"] == [user.ap_id, object.data["id"]] + assert activity.data["object"] == [user.ap_id, note_obj] assert activity.data["content"] == "blocked AND reported!!!" assert activity.data["actor"] == other_user.ap_id assert activity.data["cc"] == [user.ap_id] end + + test "it correctly processes messages with non-array to field" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => "https://www.w3.org/ns/activitystreams#Public", + "type" => "Create", + "object" => %{ + "content" => "blah blah blah", + "type" => "Note", + "attributedTo" => user.ap_id, + "inReplyTo" => nil + }, + "actor" => user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"] + end + + test "it correctly processes messages with non-array cc field" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => user.follower_address, + "cc" => "https://www.w3.org/ns/activitystreams#Public", + "type" => "Create", + "object" => %{ + "content" => "blah blah blah", + "type" => "Note", + "attributedTo" => user.ap_id, + "inReplyTo" => nil + }, + "actor" => user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"] + assert [user.follower_address] == activity.data["to"] + end end describe "prepare outgoing" do @@ -1262,32 +1341,6 @@ test "it sets the 'attributedTo' property to the actor of the object if it doesn assert modified["object"]["actor"] == modified["object"]["attributedTo"] end - test "it translates ostatus IDs to external URLs" do - incoming = File.read!("test/fixtures/incoming_note_activity.xml") - {:ok, [referent_activity]} = OStatus.handle_incoming(incoming) - - user = insert(:user) - - {:ok, activity, _} = CommonAPI.favorite(referent_activity.id, user) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["object"] == "http://gs.example.org:4040/index.php/notice/29" - end - - test "it translates ostatus reply_to IDs to external URLs" do - incoming = File.read!("test/fixtures/incoming_note_activity.xml") - {:ok, [referred_activity]} = OStatus.handle_incoming(incoming) - - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{"status" => "HI!", "in_reply_to_status_id" => referred_activity.id}) - - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["object"]["inReplyTo"] == "http://gs.example.org:4040/index.php/notice/29" - end - test "it strips internal hashtag data" do user = insert(:user) @@ -1400,25 +1453,26 @@ test "it upgrades a user to activitypub" do follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) }) - user_two = insert(:user, %{following: [user.follower_address]}) + user_two = insert(:user) + Pleroma.FollowingRelationship.follow(user_two, user, "accept") {:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) {:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"}) assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients user = User.get_cached_by_id(user.id) - assert user.info.note_count == 1 + assert user.note_count == 1 {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye") ObanHelpers.perform_all() - assert user.info.ap_enabled - assert user.info.note_count == 1 + assert user.ap_enabled + assert user.note_count == 1 assert user.follower_address == "https://niu.moe/users/rye/followers" assert user.following_address == "https://niu.moe/users/rye/following" user = User.get_cached_by_id(user.id) - assert user.info.note_count == 1 + assert user.note_count == 1 activity = Activity.get_by_id(activity.id) assert user.follower_address in activity.recipients @@ -1439,7 +1493,7 @@ test "it upgrades a user to activitypub" do "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" } ] - } = user.info.banner + } = user.banner refute "..." in activity.recipients @@ -1447,23 +1501,8 @@ test "it upgrades a user to activitypub" do refute user.follower_address in unrelated_activity.recipients user_two = User.get_cached_by_id(user_two.id) - assert user.follower_address in user_two.following - refute "..." in user_two.following - end - end - - describe "maybe_retire_websub" do - test "it deletes all websub client subscripitions with the user as topic" do - subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/rye.atom"} - {:ok, ws} = Repo.insert(subscription) - - subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/pasty.atom"} - {:ok, ws2} = Repo.insert(subscription) - - Transmogrifier.maybe_retire_websub("https://niu.moe/users/rye") - - refute Repo.get(WebsubClientSubscription, ws.id) - assert Repo.get(WebsubClientSubscription, ws2.id) + assert User.following?(user_two, user) + refute "..." in User.following(user_two) end end @@ -1489,7 +1528,9 @@ test "it rejects activities which reference objects with bogus origins" do "type" => "Announce" } - :error = Transmogrifier.handle_incoming(data) + assert capture_log(fn -> + :error = Transmogrifier.handle_incoming(data) + end) =~ "Object containment failed" end test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do @@ -1502,7 +1543,9 @@ test "it rejects activities which reference objects that have an incorrect attri "type" => "Announce" } - :error = Transmogrifier.handle_incoming(data) + assert capture_log(fn -> + :error = Transmogrifier.handle_incoming(data) + end) =~ "Object containment failed" end test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do @@ -1515,7 +1558,9 @@ test "it rejects activities which reference objects that have an incorrect attri "type" => "Announce" } - :error = Transmogrifier.handle_incoming(data) + assert capture_log(fn -> + :error = Transmogrifier.handle_incoming(data) + end) =~ "Object containment failed" end end @@ -1818,7 +1863,9 @@ test "retunrs not modified object" do describe "get_obj_helper/2" do test "returns nil when cannot normalize object" do - refute Transmogrifier.get_obj_helper("test-obj-id") + assert capture_log(fn -> + refute Transmogrifier.get_obj_helper("test-obj-id") + end) =~ "Unsupported URI scheme" end test "returns {:ok, %Object{}} for success case" do diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs index c57ea7eb9..586eb1d2f 100644 --- a/test/web/activity_pub/utils_test.exs +++ b/test/web/activity_pub/utils_test.exs @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.CommonAPI import Pleroma.Factory @@ -297,7 +298,7 @@ test "fetches only Create activities" do describe "update_follow_state_for_all/2" do test "updates the state of all Follow activities with the same actor and object" do - user = insert(:user, info: %{locked: true}) + user = insert(:user, locked: true) follower = insert(:user) {:ok, follow_activity} = ActivityPub.follow(follower, user) @@ -321,7 +322,7 @@ test "updates the state of all Follow activities with the same actor and object" describe "update_follow_state/2" do test "updates the state of the given follow activity" do - user = insert(:user, info: %{locked: true}) + user = insert(:user, locked: true) follower = insert(:user) {:ok, follow_activity} = ActivityPub.follow(follower, user) @@ -581,11 +582,19 @@ test "returns map with Flag object" do %{} ) + note_obj = %{ + "type" => "Note", + "id" => activity_ap_id, + "content" => content, + "published" => activity.object.data["published"], + "actor" => AccountView.render("show.json", %{user: target_account}) + } + assert %{ "type" => "Flag", "content" => ^content, "context" => ^context, - "object" => [^target_ap_id, ^activity_ap_id], + "object" => [^target_ap_id, ^note_obj], "state" => "open" } = res end diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs index 3155749aa..3299be2d5 100644 --- a/test/web/activity_pub/views/user_view_test.exs +++ b/test/web/activity_pub/views/user_view_test.exs @@ -29,7 +29,7 @@ test "Renders profile fields" do {:ok, user} = insert(:user) - |> User.upgrade_changeset(%{info: %{fields: fields}}) + |> User.upgrade_changeset(%{fields: fields}) |> User.update_and_set_cache() assert %{ @@ -38,7 +38,7 @@ test "Renders profile fields" do end test "Renders with emoji tags" do - user = insert(:user, %{info: %{emoji: [%{"bib" => "/test"}]}}) + user = insert(:user, emoji: [%{"bib" => "/test"}]) assert %{ "tag" => [ @@ -64,9 +64,7 @@ test "Does not add an avatar image if the user hasn't set one" do user = insert(:user, avatar: %{"url" => [%{"href" => "https://someurl"}]}, - info: %{ - banner: %{"url" => [%{"href" => "https://somebanner"}]} - } + banner: %{"url" => [%{"href" => "https://somebanner"}]} ) {:ok, user} = User.ensure_keys_present(user) @@ -76,6 +74,12 @@ test "Does not add an avatar image if the user hasn't set one" do assert result["image"]["url"] == "https://somebanner" end + test "renders an invisible user with the invisible property set to true" do + user = insert(:user, invisible: true) + + assert %{"invisible" => true} = UserView.render("service.json", %{user: user}) + end + describe "endpoints" do test "local users have a usable endpoints structure" do user = insert(:user) @@ -121,8 +125,7 @@ test "sets totalItems to zero when followers are hidden" do other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) - info = Map.merge(user.info, %{hide_followers_count: true, hide_followers: true}) - user = Map.put(user, :info, info) + user = Map.merge(user, %{hide_followers_count: true, hide_followers: true}) assert %{"totalItems" => 0} = UserView.render("followers.json", %{user: user}) end @@ -131,8 +134,7 @@ test "sets correct totalItems when followers are hidden but the follower counter other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) - info = Map.merge(user.info, %{hide_followers_count: false, hide_followers: true}) - user = Map.put(user, :info, info) + user = Map.merge(user, %{hide_followers_count: false, hide_followers: true}) assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user}) end end @@ -143,8 +145,7 @@ test "sets totalItems to zero when follows are hidden" do other_user = insert(:user) {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) - info = Map.merge(user.info, %{hide_follows_count: true, hide_follows: true}) - user = Map.put(user, :info, info) + user = Map.merge(user, %{hide_follows_count: true, hide_follows: true}) assert %{"totalItems" => 0} = UserView.render("following.json", %{user: user}) end @@ -153,8 +154,7 @@ test "sets correct totalItems when follows are hidden but the follow counter is other_user = insert(:user) {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) - info = Map.merge(user.info, %{hide_follows_count: false, hide_follows: true}) - user = Map.put(user, :info, info) + user = Map.merge(user, %{hide_follows_count: false, hide_follows: true}) assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) end end diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs index b62a89e68..4c2e0d207 100644 --- a/test/web/activity_pub/visibilty_test.exs +++ b/test/web/activity_pub/visibilty_test.exs @@ -212,7 +212,8 @@ test "returns false when invalid recipients", %{user: user} do test "returns true if user following to author" do author = insert(:user) - user = insert(:user, following: [author.ap_id]) + user = insert(:user) + Pleroma.User.follow(user, author) activity = insert(:note_activity, diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index b5c355e66..2a9e4f5a0 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -13,13 +13,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.UserInviteToken + alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.CommonAPI alias Pleroma.Web.MediaProxy import Pleroma.Factory - describe "/api/pleroma/admin/users" do - test "Delete" do - admin = insert(:user, info: %{is_admin: true}) + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + :ok + end + + describe "DELETE /api/pleroma/admin/users" do + test "single user" do + admin = insert(:user, is_admin: true) user = insert(:user) conn = @@ -30,17 +37,38 @@ test "Delete" do log_entry = Repo.one(ModerationLog) - assert log_entry.data["subject"]["nickname"] == user.nickname - assert log_entry.data["action"] == "delete" - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deleted user @#{user.nickname}" + "@#{admin.nickname} deleted users: @#{user.nickname}" assert json_response(conn, 200) == user.nickname end + test "multiple users" do + admin = insert(:user, is_admin: true) + user_one = insert(:user) + user_two = insert(:user) + + conn = + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users", %{ + nicknames: [user_one.nickname, user_two.nickname] + }) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}" + + response = json_response(conn, 200) + assert response -- [user_one.nickname, user_two.nickname] == [] + end + end + + describe "/api/pleroma/admin/users" do test "Create" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) conn = build_conn() @@ -70,7 +98,7 @@ test "Create" do end test "Cannot create user with exisiting email" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = insert(:user) conn = @@ -101,7 +129,7 @@ test "Cannot create user with exisiting email" do end test "Cannot create user with exisiting nickname" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = insert(:user) conn = @@ -132,7 +160,7 @@ test "Cannot create user with exisiting nickname" do end test "Multiple user creation works in transaction" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = insert(:user) conn = @@ -181,7 +209,7 @@ test "Multiple user creation works in transaction" do describe "/api/pleroma/admin/users/:nickname" do test "Show", %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = insert(:user) conn = @@ -204,7 +232,7 @@ test "Show", %{conn: conn} do end test "when the user doesn't exist", %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = build(:user) conn = @@ -218,7 +246,7 @@ test "when the user doesn't exist", %{conn: conn} do describe "/api/pleroma/admin/users/follow" do test "allows to force-follow another user" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = insert(:user) follower = insert(:user) @@ -244,7 +272,7 @@ test "allows to force-follow another user" do describe "/api/pleroma/admin/users/unfollow" do test "allows to force-unfollow another user" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = insert(:user) follower = insert(:user) @@ -272,7 +300,7 @@ test "allows to force-unfollow another user" do describe "PUT /api/pleroma/admin/users/tag" do setup do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user1 = insert(:user, %{tags: ["x"]}) user2 = insert(:user, %{tags: ["y"]}) user3 = insert(:user, %{tags: ["unchanged"]}) @@ -321,7 +349,7 @@ test "it does not modify tags of not specified users", %{conn: conn, user3: user describe "DELETE /api/pleroma/admin/users/tag" do setup do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user1 = insert(:user, %{tags: ["x"]}) user2 = insert(:user, %{tags: ["y", "z"]}) user3 = insert(:user, %{tags: ["unchanged"]}) @@ -370,7 +398,7 @@ test "it does not modify tags of not specified users", %{conn: conn, user3: user describe "/api/pleroma/admin/users/:nickname/permission_group" do test "GET is giving user_info" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) conn = build_conn() @@ -385,7 +413,7 @@ test "GET is giving user_info" do end test "/:right POST, can add to a permission group" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = insert(:user) conn = @@ -404,9 +432,32 @@ test "/:right POST, can add to a permission group" do "@#{admin.nickname} made @#{user.nickname} admin" end + test "/:right POST, can add to a permission group (multiple)" do + admin = insert(:user, is_admin: true) + user_one = insert(:user) + user_two = insert(:user) + + conn = + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/users/permission_group/admin", %{ + nicknames: [user_one.nickname, user_two.nickname] + }) + + assert json_response(conn, 200) == %{ + "is_admin" => true + } + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin" + end + test "/:right DELETE, can remove from a permission group" do - admin = insert(:user, info: %{is_admin: true}) - user = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) + user = insert(:user, is_admin: true) conn = build_conn() @@ -423,69 +474,36 @@ test "/:right DELETE, can remove from a permission group" do assert ModerationLog.get_log_entry_message(log_entry) == "@#{admin.nickname} revoked admin role from @#{user.nickname}" end - end - describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do - setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + test "/:right DELETE, can remove from a permission group (multiple)" do + admin = insert(:user, is_admin: true) + user_one = insert(:user, is_admin: true) + user_two = insert(:user, is_admin: true) conn = - conn + build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users/permission_group/admin", %{ + nicknames: [user_one.nickname, user_two.nickname] + }) - %{conn: conn, admin: admin} - end - - test "deactivates the user", %{conn: conn, admin: admin} do - user = insert(:user) - - conn = - conn - |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false}) - - user = User.get_cached_by_id(user.id) - assert user.info.deactivated == true - assert json_response(conn, :no_content) + assert json_response(conn, 200) == %{ + "is_admin" => false + } log_entry = Repo.one(ModerationLog) assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deactivated user @#{user.nickname}" - end - - test "activates the user", %{conn: conn, admin: admin} do - user = insert(:user, info: %{deactivated: true}) - - conn = - conn - |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true}) - - user = User.get_cached_by_id(user.id) - assert user.info.deactivated == false - assert json_response(conn, :no_content) - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} activated user @#{user.nickname}" - end - - test "returns 403 when requested by a non-admin", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false}) - - assert json_response(conn, :forbidden) + "@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{ + user_two.nickname + }" end end describe "POST /api/pleroma/admin/email_invite, with valid config" do setup do - [user: insert(:user, info: %{is_admin: true})] + [user: insert(:user, is_admin: true)] end clear_config([:instance, :registrations_open]) do @@ -545,7 +563,7 @@ test "it returns 403 if requested by a non-admin", %{conn: conn} do describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do setup do - [user: insert(:user, info: %{is_admin: true})] + [user: insert(:user, is_admin: true)] end clear_config([:instance, :registrations_open]) @@ -577,7 +595,7 @@ test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: us end test "/api/pleroma/admin/users/:nickname/password_reset" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = insert(:user) conn = @@ -593,7 +611,7 @@ test "/api/pleroma/admin/users/:nickname/password_reset" do describe "GET /api/pleroma/admin/users" do setup do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) conn = build_conn() @@ -609,7 +627,7 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do users = [ %{ - "deactivated" => admin.info.deactivated, + "deactivated" => admin.deactivated, "id" => admin.id, "nickname" => admin.nickname, "roles" => %{"admin" => true, "moderator" => false}, @@ -619,7 +637,7 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do "display_name" => HTML.strip_tags(admin.name || admin.nickname) }, %{ - "deactivated" => user.info.deactivated, + "deactivated" => user.deactivated, "id" => user.id, "nickname" => user.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -660,7 +678,7 @@ test "regular search", %{conn: conn} do "page_size" => 50, "users" => [ %{ - "deactivated" => user.info.deactivated, + "deactivated" => user.deactivated, "id" => user.id, "nickname" => user.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -684,7 +702,7 @@ test "search by domain", %{conn: conn} do "page_size" => 50, "users" => [ %{ - "deactivated" => user.info.deactivated, + "deactivated" => user.deactivated, "id" => user.id, "nickname" => user.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -708,7 +726,7 @@ test "search by full nickname", %{conn: conn} do "page_size" => 50, "users" => [ %{ - "deactivated" => user.info.deactivated, + "deactivated" => user.deactivated, "id" => user.id, "nickname" => user.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -732,7 +750,7 @@ test "search by display name", %{conn: conn} do "page_size" => 50, "users" => [ %{ - "deactivated" => user.info.deactivated, + "deactivated" => user.deactivated, "id" => user.id, "nickname" => user.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -756,7 +774,7 @@ test "search by email", %{conn: conn} do "page_size" => 50, "users" => [ %{ - "deactivated" => user.info.deactivated, + "deactivated" => user.deactivated, "id" => user.id, "nickname" => user.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -780,7 +798,7 @@ test "regular search with page size", %{conn: conn} do "page_size" => 1, "users" => [ %{ - "deactivated" => user.info.deactivated, + "deactivated" => user.deactivated, "id" => user.id, "nickname" => user.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -799,7 +817,7 @@ test "regular search with page size", %{conn: conn} do "page_size" => 1, "users" => [ %{ - "deactivated" => user2.info.deactivated, + "deactivated" => user2.deactivated, "id" => user2.id, "nickname" => user2.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -813,7 +831,7 @@ test "regular search with page size", %{conn: conn} do end test "only local users" do - admin = insert(:user, info: %{is_admin: true}, nickname: "john") + admin = insert(:user, is_admin: true, nickname: "john") user = insert(:user, nickname: "bob") insert(:user, nickname: "bobb", local: false) @@ -828,7 +846,7 @@ test "only local users" do "page_size" => 50, "users" => [ %{ - "deactivated" => user.info.deactivated, + "deactivated" => user.deactivated, "id" => user.id, "nickname" => user.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -842,7 +860,7 @@ test "only local users" do end test "only local users with no query", %{admin: old_admin} do - admin = insert(:user, info: %{is_admin: true}, nickname: "john") + admin = insert(:user, is_admin: true, nickname: "john") user = insert(:user, nickname: "bob") insert(:user, nickname: "bobb", local: false) @@ -855,7 +873,7 @@ test "only local users with no query", %{admin: old_admin} do users = [ %{ - "deactivated" => user.info.deactivated, + "deactivated" => user.deactivated, "id" => user.id, "nickname" => user.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -865,7 +883,7 @@ test "only local users with no query", %{admin: old_admin} do "display_name" => HTML.strip_tags(user.name || user.nickname) }, %{ - "deactivated" => admin.info.deactivated, + "deactivated" => admin.deactivated, "id" => admin.id, "nickname" => admin.nickname, "roles" => %{"admin" => true, "moderator" => false}, @@ -895,7 +913,7 @@ test "only local users with no query", %{admin: old_admin} do end test "load only admins", %{conn: conn, admin: admin} do - second_admin = insert(:user, info: %{is_admin: true}) + second_admin = insert(:user, is_admin: true) insert(:user) insert(:user) @@ -934,7 +952,7 @@ test "load only admins", %{conn: conn, admin: admin} do end test "load only moderators", %{conn: conn} do - moderator = insert(:user, info: %{is_moderator: true}) + moderator = insert(:user, is_moderator: true) insert(:user) insert(:user) @@ -999,11 +1017,11 @@ test "load users with tags list", %{conn: conn} do end test "it works with multiple filters" do - admin = insert(:user, nickname: "john", info: %{is_admin: true}) - user = insert(:user, nickname: "bob", local: false, info: %{deactivated: true}) + admin = insert(:user, nickname: "john", is_admin: true) + user = insert(:user, nickname: "bob", local: false, deactivated: true) - insert(:user, nickname: "ken", local: true, info: %{deactivated: true}) - insert(:user, nickname: "bobb", local: false, info: %{deactivated: false}) + insert(:user, nickname: "ken", local: true, deactivated: true) + insert(:user, nickname: "bobb", local: false, deactivated: false) conn = build_conn() @@ -1015,7 +1033,7 @@ test "it works with multiple filters" do "page_size" => 50, "users" => [ %{ - "deactivated" => user.info.deactivated, + "deactivated" => user.deactivated, "id" => user.id, "nickname" => user.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -1027,10 +1045,80 @@ test "it works with multiple filters" do ] } end + + test "it omits relay user", %{admin: admin} do + assert %User{} = Relay.get_actor() + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/users") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => admin.deactivated, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => true, + "tags" => [], + "avatar" => User.avatar_url(admin) |> MediaProxy.url(), + "display_name" => HTML.strip_tags(admin.name || admin.nickname) + } + ] + } + end + end + + test "PATCH /api/pleroma/admin/users/activate" do + admin = insert(:user, is_admin: true) + user_one = insert(:user, deactivated: true) + user_two = insert(:user, deactivated: true) + + conn = + build_conn() + |> assign(:user, admin) + |> patch( + "/api/pleroma/admin/users/activate", + %{nicknames: [user_one.nickname, user_two.nickname]} + ) + + response = json_response(conn, 200) + assert Enum.map(response["users"], & &1["deactivated"]) == [false, false] + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}" + end + + test "PATCH /api/pleroma/admin/users/deactivate" do + admin = insert(:user, is_admin: true) + user_one = insert(:user, deactivated: false) + user_two = insert(:user, deactivated: false) + + conn = + build_conn() + |> assign(:user, admin) + |> patch( + "/api/pleroma/admin/users/deactivate", + %{nicknames: [user_one.nickname, user_two.nickname]} + ) + + response = json_response(conn, 200) + assert Enum.map(response["users"], & &1["deactivated"]) == [true, true] + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}" end test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = insert(:user) conn = @@ -1040,7 +1128,7 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do assert json_response(conn, 200) == %{ - "deactivated" => !user.info.deactivated, + "deactivated" => !user.deactivated, "id" => user.id, "nickname" => user.nickname, "roles" => %{"admin" => false, "moderator" => false}, @@ -1053,12 +1141,12 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do log_entry = Repo.one(ModerationLog) assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deactivated user @#{user.nickname}" + "@#{admin.nickname} deactivated users: @#{user.nickname}" end describe "POST /api/pleroma/admin/users/invite_token" do setup do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) conn = build_conn() @@ -1122,7 +1210,7 @@ test "with max use and expires_at", %{conn: conn} do describe "GET /api/pleroma/admin/users/invites" do setup do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) conn = build_conn() @@ -1160,7 +1248,7 @@ test "with invite", %{conn: conn} do describe "POST /api/pleroma/admin/users/revoke_invite" do test "with token" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) {:ok, invite} = UserInviteToken.create_invite() conn = @@ -1180,7 +1268,7 @@ test "with token" do end test "with invalid token" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) conn = build_conn() @@ -1193,7 +1281,7 @@ test "with invalid token" do describe "GET /api/pleroma/admin/reports/:id" do setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) %{conn: assign(conn, :user, admin)} end @@ -1226,7 +1314,7 @@ test "returns 404 when report id is invalid", %{conn: conn} do describe "PUT /api/pleroma/admin/reports/:id" do setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) @@ -1287,7 +1375,7 @@ test "returns 404 when report is not exist", %{conn: conn} do describe "GET /api/pleroma/admin/reports" do setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) %{conn: assign(conn, :user, admin)} end @@ -1407,7 +1495,7 @@ test "returns 403 when requested by anonymous" do # describe "POST /api/pleroma/admin/reports/:id/respond" do setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) %{conn: assign(conn, :user, admin), admin: admin} end @@ -1462,7 +1550,7 @@ test "returns 404 when report id is invalid", %{conn: conn} do describe "PUT /api/pleroma/admin/statuses/:id" do setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) activity = insert(:note_activity) %{conn: assign(conn, :user, admin), id: activity.id, admin: admin} @@ -1528,7 +1616,7 @@ test "returns 400 when visibility is unknown", %{conn: conn, id: id} do describe "DELETE /api/pleroma/admin/statuses/:id" do setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) activity = insert(:note_activity) %{conn: assign(conn, :user, admin), id: activity.id, admin: admin} @@ -1558,7 +1646,7 @@ test "returns error when status is not exist", %{conn: conn} do describe "GET /api/pleroma/admin/config" do setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) %{conn: assign(conn, :user, admin)} end @@ -1595,7 +1683,7 @@ test "with settings in db", %{conn: conn} do describe "POST /api/pleroma/admin/config" do setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) temp_file = "config/test.exported_from_db.secret.exs" @@ -2163,7 +2251,7 @@ test "delete part of settings by atom subkeys", %{conn: conn} do describe "config mix tasks run" do setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) temp_file = "config/test.exported_from_db.secret.exs" @@ -2199,7 +2287,7 @@ test "transfer settings to DB and to file", %{conn: conn, admin: admin} do describe "GET /api/pleroma/admin/users/:nickname/statuses" do setup do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = insert(:user) date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!() @@ -2256,8 +2344,8 @@ test "returns private statuses with godmode on", %{conn: conn, user: user} do describe "GET /api/pleroma/admin/moderation_log" do setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) - moderator = insert(:user, info: %{is_moderator: true}) + admin = insert(:user, is_admin: true) + moderator = insert(:user, is_moderator: true) %{conn: assign(conn, :user, admin), admin: admin, moderator: moderator} end @@ -2465,25 +2553,91 @@ test "returns log filtered by search", %{conn: conn, moderator: moderator} do describe "PATCH /users/:nickname/force_password_reset" do setup %{conn: conn} do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) user = insert(:user) %{conn: assign(conn, :user, admin), admin: admin, user: user} end test "sets password_reset_pending to true", %{admin: admin, user: user} do - assert user.info.password_reset_pending == false + assert user.password_reset_pending == false conn = build_conn() |> assign(:user, admin) - |> patch("/api/pleroma/admin/users/#{user.nickname}/force_password_reset") + |> patch("/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]}) assert json_response(conn, 204) == "" ObanHelpers.perform_all() - assert User.get_by_id(user.id).info.password_reset_pending == true + assert User.get_by_id(user.id).password_reset_pending == true + end + end + + describe "relays" do + setup %{conn: conn} do + admin = insert(:user, is_admin: true) + + %{conn: assign(conn, :user, admin), admin: admin} + end + + test "POST /relay", %{admin: admin} do + conn = + build_conn() + |> assign(:user, admin) + |> post("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + assert json_response(conn, 200) == "http://mastodon.example.org/users/admin" + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" + end + + test "GET /relay", %{admin: admin} do + relay_user = Pleroma.Web.ActivityPub.Relay.get_actor() + + ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] + |> Enum.each(fn ap_id -> + {:ok, user} = User.get_or_fetch_by_ap_id(ap_id) + User.follow(relay_user, user) + end) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/relay") + + assert json_response(conn, 200)["relays"] -- ["mastodon.example.org", "mstdn.io"] == [] + end + + test "DELETE /relay", %{admin: admin} do + build_conn() + |> assign(:user, admin) + |> post("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + conn = + build_conn() + |> assign(:user, admin) + |> delete("/api/pleroma/admin/relay", %{ + relay_url: "http://mastodon.example.org/users/admin" + }) + + assert json_response(conn, 200) == "http://mastodon.example.org/users/admin" + + [log_entry_one, log_entry_two] = Repo.all(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry_one) == + "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" + + assert ModerationLog.get_log_entry_message(log_entry_two) == + "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin" end end end diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs index 9df4cd539..082e691c4 100644 --- a/test/web/admin_api/search_test.exs +++ b/test/web/admin_api/search_test.exs @@ -47,9 +47,9 @@ test "it returns local/external users" do end test "it returns active/deactivated users" do - insert(:user, info: %{deactivated: true}) - insert(:user, info: %{deactivated: true}) - insert(:user, info: %{deactivated: false}) + insert(:user, deactivated: true) + insert(:user, deactivated: true) + insert(:user, deactivated: false) {:ok, _results, active_count} = Search.user(%{ @@ -70,7 +70,7 @@ test "it returns active/deactivated users" do test "it returns specific user" do insert(:user) insert(:user) - user = insert(:user, nickname: "bob", local: true, info: %{deactivated: false}) + user = insert(:user, nickname: "bob", local: true, deactivated: false) {:ok, _results, total_count} = Search.user(%{query: ""}) @@ -108,7 +108,7 @@ test "it return user by full nickname" do end test "it returns admin user" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) insert(:user) insert(:user) @@ -119,7 +119,7 @@ test "it returns admin user" do end test "it returns moderator user" do - moderator = insert(:user, info: %{is_moderator: true}) + moderator = insert(:user, is_moderator: true) insert(:user) insert(:user) diff --git a/test/web/admin_api/views/report_view_test.exs b/test/web/admin_api/views/report_view_test.exs index 475705857..ef4a806e4 100644 --- a/test/web/admin_api/views/report_view_test.exs +++ b/test/web/admin_api/views/report_view_test.exs @@ -49,6 +49,8 @@ test "includes reported statuses" do {:ok, report_activity} = CommonAPI.report(user, %{"account_id" => other_user.id, "status_ids" => [activity.id]}) + other_user = Pleroma.User.get_by_id(other_user.id) + expected = %{ content: nil, actor: diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index cc1d122d9..09a3e8dcf 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -10,6 +10,7 @@ defmodule Pleroma.Web.CommonAPITest do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.CommonAPI import Pleroma.Factory @@ -100,7 +101,7 @@ test "it adds emoji when updating profiles" do {:ok, activity} = CommonAPI.update(user) user = User.get_cached_by_ap_id(user.ap_id) - [firefox] = user.info.source_data["tag"] + [firefox] = user.source_data["tag"] assert firefox["name"] == ":firefox:" @@ -140,7 +141,7 @@ test "it filters out obviously bad tags when accepting a post as HTML" do object = Object.normalize(activity) - assert object.data["content"] == "

2hu

alert('xss')" + assert object.data["content"] == "

2hu

alert('xss')" end test "it filters out obviously bad tags when accepting a post as Markdown" do @@ -156,7 +157,7 @@ test "it filters out obviously bad tags when accepting a post as Markdown" do object = Object.normalize(activity) - assert object.data["content"] == "

2hu

alert('xss')" + assert object.data["content"] == "

2hu

alert('xss')" end test "it does not allow replies to direct messages that are not direct messages themselves" do @@ -318,7 +319,7 @@ test "pin status", %{user: user, activity: activity} do id = activity.id user = refresh_record(user) - assert %User{info: %{pinned_activities: [^id]}} = user + assert %User{pinned_activities: [^id]} = user end test "unlisted statuses can be pinned", %{user: user} do @@ -352,7 +353,7 @@ test "unpin status", %{user: user, activity: activity} do user = refresh_record(user) - assert %User{info: %{pinned_activities: []}} = user + assert %User{pinned_activities: []} = user end test "should unpin when deleting a status", %{user: user, activity: activity} do @@ -364,7 +365,7 @@ test "should unpin when deleting a status", %{user: user, activity: activity} do user = refresh_record(user) - assert %User{info: %{pinned_activities: []}} = user + assert %User{pinned_activities: []} = user end end @@ -412,6 +413,14 @@ test "creates a report" do "status_ids" => [activity.id] } + note_obj = %{ + "type" => "Note", + "id" => activity_ap_id, + "content" => "foobar", + "published" => activity.object.data["published"], + "actor" => AccountView.render("show.json", %{user: target_user}) + } + assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data) assert %Activity{ @@ -419,7 +428,7 @@ test "creates a report" do data: %{ "type" => "Flag", "content" => ^comment, - "object" => [^target_ap_id, ^activity_ap_id], + "object" => [^target_ap_id, ^note_obj], "state" => "open" } } = flag_activity @@ -439,6 +448,11 @@ test "updates report state" do {:ok, report} = CommonAPI.update_report_state(report_id, "resolved") assert report.data["state"] == "resolved" + + [reported_user, activity_id] = report.data["object"] + + assert reported_user == target_user.ap_id + assert activity_id == activity.data["id"] end test "does not update report state when state is unsupported" do @@ -495,7 +509,7 @@ test "also unsubscribes a user" do describe "accept_follow_request/2" do test "after acceptance, it sets all existing pending follow request states to 'accept'" do - user = insert(:user, info: %{locked: true}) + user = insert(:user, locked: true) follower = insert(:user) follower_two = insert(:user) @@ -515,7 +529,7 @@ test "after acceptance, it sets all existing pending follow request states to 'a end test "after rejection, it sets all existing pending follow request states to 'reject'" do - user = insert(:user, info: %{locked: true}) + user = insert(:user, locked: true) follower = insert(:user) follower_two = insert(:user) diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs index 43a715706..c224197c3 100644 --- a/test/web/federator_test.exs +++ b/test/web/federator_test.exs @@ -81,14 +81,16 @@ test "it federates only to reachable instances via AP" do local: false, nickname: "nick1@domain.com", ap_id: "https://domain.com/users/nick1", - info: %{ap_enabled: true, source_data: %{"inbox" => inbox1}} + source_data: %{"inbox" => inbox1}, + ap_enabled: true }) insert(:user, %{ local: false, nickname: "nick2@domain2.com", ap_id: "https://domain2.com/users/nick2", - info: %{ap_enabled: true, source_data: %{"inbox" => inbox2}} + source_data: %{"inbox" => inbox2}, + ap_enabled: true }) dt = NaiveDateTime.utc_now() @@ -111,93 +113,6 @@ test "it federates only to reachable instances via AP" do all_enqueued(worker: PublisherWorker) ) end - - test "it federates only to reachable instances via Websub" do - user = insert(:user) - websub_topic = Pleroma.Web.OStatus.feed_path(user) - - sub1 = - insert(:websub_subscription, %{ - topic: websub_topic, - state: "active", - callback: "http://pleroma.soykaf.com/cb" - }) - - sub2 = - insert(:websub_subscription, %{ - topic: websub_topic, - state: "active", - callback: "https://pleroma2.soykaf.com/cb" - }) - - dt = NaiveDateTime.utc_now() - Instances.set_unreachable(sub2.callback, dt) - - Instances.set_consistently_unreachable(sub1.callback) - - {:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"}) - - expected_callback = sub2.callback - expected_dt = NaiveDateTime.to_iso8601(dt) - - ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) - - assert ObanHelpers.member?( - %{ - "op" => "publish_one", - "params" => %{ - "callback" => expected_callback, - "unreachable_since" => expected_dt - } - }, - all_enqueued(worker: PublisherWorker) - ) - end - - test "it federates only to reachable instances via Salmon" do - user = insert(:user) - - _remote_user1 = - insert(:user, %{ - local: false, - nickname: "nick1@domain.com", - ap_id: "https://domain.com/users/nick1", - info: %{salmon: "https://domain.com/salmon"} - }) - - remote_user2 = - insert(:user, %{ - local: false, - nickname: "nick2@domain2.com", - ap_id: "https://domain2.com/users/nick2", - info: %{salmon: "https://domain2.com/salmon"} - }) - - remote_user2_id = remote_user2.id - - dt = NaiveDateTime.utc_now() - Instances.set_unreachable(remote_user2.ap_id, dt) - - Instances.set_consistently_unreachable("domain.com") - - {:ok, _activity} = - CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"}) - - expected_dt = NaiveDateTime.to_iso8601(dt) - - ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) - - assert ObanHelpers.member?( - %{ - "op" => "publish_one", - "params" => %{ - "recipient_id" => remote_user2_id, - "unreachable_since" => expected_dt - } - }, - all_enqueued(worker: PublisherWorker) - ) - end end describe "Receive an activity" do diff --git a/test/web/masto_fe_controller_test.exs b/test/web/masto_fe_controller_test.exs index ab9dab352..b5dbd4a25 100644 --- a/test/web/masto_fe_controller_test.exs +++ b/test/web/masto_fe_controller_test.exs @@ -23,7 +23,7 @@ test "put settings", %{conn: conn} do assert _result = json_response(conn, 200) user = User.get_cached_by_ap_id(user.ap_id) - assert user.info.settings == %{"programming" => "socks"} + assert user.settings == %{"programming" => "socks"} end describe "index/2 redirections" do 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 618031b40..519b56d6c 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 @@ -153,7 +153,7 @@ test "updates the user's skip_thread_containment option", %{conn: conn} do |> json_response(200) assert response["pleroma"]["skip_thread_containment"] == true - assert refresh_record(user).info.skip_thread_containment + assert refresh_record(user).skip_thread_containment end test "updates the user's hide_follows status", %{conn: conn} do diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index 6a59c3d94..8fc2d9300 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -237,6 +237,20 @@ test "filters user's statuses by a hashtag", %{conn: conn} do assert [%{"id" => id}] = json_response(conn, 200) assert id == to_string(post.id) end + + test "the user views their own timelines and excludes direct messages", %{conn: conn} do + user = insert(:user) + {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"}) + {:ok, _direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_visibilities" => ["direct"]}) + + assert [%{"id" => id}] = json_response(conn, 200) + assert id == to_string(public_activity.id) + end end describe "followers" do @@ -255,7 +269,7 @@ test "getting followers", %{conn: conn} do test "getting followers, hide_followers", %{conn: conn} do user = insert(:user) - other_user = insert(:user, %{info: %{hide_followers: true}}) + other_user = insert(:user, hide_followers: true) {:ok, _user} = User.follow(user, other_user) conn = @@ -267,7 +281,7 @@ test "getting followers, hide_followers", %{conn: conn} do test "getting followers, hide_followers, same user requesting", %{conn: conn} do user = insert(:user) - other_user = insert(:user, %{info: %{hide_followers: true}}) + other_user = insert(:user, hide_followers: true) {:ok, _user} = User.follow(user, other_user) conn = @@ -335,7 +349,7 @@ test "getting following", %{conn: conn} do end test "getting following, hide_follows", %{conn: conn} do - user = insert(:user, %{info: %{hide_follows: true}}) + user = insert(:user, hide_follows: true) other_user = insert(:user) {:ok, user} = User.follow(user, other_user) @@ -347,7 +361,7 @@ test "getting following, hide_follows", %{conn: conn} do end test "getting following, hide_follows, same user requesting", %{conn: conn} do - user = insert(:user, %{info: %{hide_follows: true}}) + user = insert(:user, hide_follows: true) other_user = insert(:user) {:ok, user} = User.follow(user, other_user) @@ -457,7 +471,7 @@ test "following without reblogs" do conn = build_conn() - |> assign(:user, follower) + |> assign(:user, User.get_cached_by_id(follower.id)) |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true") assert %{"showing_reblogs" => true} = json_response(conn, 200) @@ -669,7 +683,7 @@ test "Account registration via Application", %{conn: conn} do token_from_db = Repo.preload(token_from_db, :user) assert token_from_db.user - assert token_from_db.user.info.confirmation_pending + assert token_from_db.user.confirmation_pending end test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do @@ -713,7 +727,7 @@ test "rate limit", %{conn: conn} do token_from_db = Repo.preload(token_from_db, :user) assert token_from_db.user - assert token_from_db.user.info.confirmation_pending + assert token_from_db.user.confirmation_pending end conn = @@ -798,7 +812,7 @@ test "verify_credentials", %{conn: conn} do end test "verify_credentials default scope unlisted", %{conn: conn} do - user = insert(:user, %{info: %User.Info{default_scope: "unlisted"}}) + user = insert(:user, default_scope: "unlisted") conn = conn @@ -810,7 +824,7 @@ test "verify_credentials default scope unlisted", %{conn: conn} do end test "locked accounts", %{conn: conn} do - user = insert(:user, %{info: %User.Info{default_scope: "private"}}) + user = insert(:user, default_scope: "private") conn = conn diff --git a/test/web/mastodon_api/controllers/conversation_controller_test.exs b/test/web/mastodon_api/controllers/conversation_controller_test.exs index a308a7620..2a1223b18 100644 --- a/test/web/mastodon_api/controllers/conversation_controller_test.exs +++ b/test/web/mastodon_api/controllers/conversation_controller_test.exs @@ -17,7 +17,7 @@ test "returns a list of conversations", %{conn: conn} do {:ok, user_two} = User.follow(user_two, user_one) - assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 {:ok, direct} = CommonAPI.post(user_one, %{ @@ -25,7 +25,7 @@ test "returns a list of conversations", %{conn: conn} do "visibility" => "direct" }) - assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 1 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 {:ok, _follower_only} = CommonAPI.post(user_one, %{ @@ -54,9 +54,62 @@ test "returns a list of conversations", %{conn: conn} do assert user_two.id in account_ids assert user_three.id in account_ids assert is_binary(res_id) - assert unread == true + assert unread == false assert res_last_status["id"] == direct.id - assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1 + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 + end + + test "filters conversations by recipients", %{conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + user_three = insert(:user) + + {:ok, direct1} = + CommonAPI.post(user_one, %{ + "status" => "Hi @#{user_two.nickname}!", + "visibility" => "direct" + }) + + {:ok, _direct2} = + CommonAPI.post(user_one, %{ + "status" => "Hi @#{user_three.nickname}!", + "visibility" => "direct" + }) + + {:ok, direct3} = + CommonAPI.post(user_one, %{ + "status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!", + "visibility" => "direct" + }) + + {:ok, _direct4} = + CommonAPI.post(user_two, %{ + "status" => "Hi @#{user_three.nickname}!", + "visibility" => "direct" + }) + + {:ok, direct5} = + CommonAPI.post(user_two, %{ + "status" => "Hi @#{user_one.nickname}!", + "visibility" => "direct" + }) + + [conversation1, conversation2] = + conn + |> assign(:user, user_one) + |> get("/api/v1/conversations", %{"recipients" => [user_two.id]}) + |> json_response(200) + + assert conversation1["last_status"]["id"] == direct5.id + assert conversation2["last_status"]["id"] == direct1.id + + [conversation1] = + conn + |> assign(:user, user_one) + |> get("/api/v1/conversations", %{"recipients" => [user_two.id, user_three.id]}) + |> json_response(200) + + assert conversation1["last_status"]["id"] == direct3.id end test "updates the last_status on reply", %{conn: conn} do @@ -95,19 +148,23 @@ test "the user marks a conversation as read", %{conn: conn} do "visibility" => "direct" }) + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 + [%{"id" => direct_conversation_id, "unread" => true}] = conn - |> assign(:user, user_one) + |> assign(:user, user_two) |> get("/api/v1/conversations") |> json_response(200) %{"unread" => false} = conn - |> assign(:user, user_one) + |> assign(:user, user_two) |> post("/api/v1/conversations/#{direct_conversation_id}/read") |> json_response(200) - assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 0 + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 # The conversation is marked as unread on reply {:ok, _} = @@ -123,7 +180,8 @@ test "the user marks a conversation as read", %{conn: conn} do |> get("/api/v1/conversations") |> json_response(200) - assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1 + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 # A reply doesn't increment the user's unread_conversation_count if the conversation is unread {:ok, _} = @@ -133,7 +191,8 @@ test "the user marks a conversation as read", %{conn: conn} do "in_reply_to_status_id" => direct.id }) - assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1 + assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 + assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 end test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs index 4bf292df5..288cd9029 100644 --- a/test/web/mastodon_api/controllers/follow_request_controller_test.exs +++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs @@ -12,13 +12,11 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do describe "locked accounts" do test "/api/v1/follow_requests works" do - user = insert(:user, %{info: %User.Info{locked: true}}) + user = insert(:user, locked: true) other_user = insert(:user) {:ok, _activity} = ActivityPub.follow(other_user, user) - - user = User.get_cached_by_id(user.id) - other_user = User.get_cached_by_id(other_user.id) + {:ok, other_user} = User.follow(other_user, user, "pending") assert User.following?(other_user, user) == false @@ -32,10 +30,11 @@ test "/api/v1/follow_requests works" do end test "/api/v1/follow_requests/:id/authorize works" do - user = insert(:user, %{info: %User.Info{locked: true}}) + user = insert(:user, locked: true) other_user = insert(:user) {:ok, _activity} = ActivityPub.follow(other_user, user) + {:ok, other_user} = User.follow(other_user, user, "pending") user = User.get_cached_by_id(user.id) other_user = User.get_cached_by_id(other_user.id) @@ -57,7 +56,7 @@ test "/api/v1/follow_requests/:id/authorize works" do end test "/api/v1/follow_requests/:id/reject works" do - user = insert(:user, %{info: %User.Info{locked: true}}) + user = insert(:user, locked: true) other_user = insert(:user) {:ok, _activity} = ActivityPub.follow(other_user, user) diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs index f8049f81f..e00de6b18 100644 --- a/test/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/web/mastodon_api/controllers/instance_controller_test.exs @@ -41,20 +41,13 @@ test "get instance stats", %{conn: conn} do user = insert(:user, %{local: true}) user2 = insert(:user, %{local: true}) - {:ok, _user2} = User.deactivate(user2, !user2.info.deactivated) + {:ok, _user2} = User.deactivate(user2, !user2.deactivated) insert(:user, %{local: false, nickname: "u@peer1.com"}) insert(:user, %{local: false, nickname: "u@peer2.com"}) {:ok, _} = Pleroma.Web.CommonAPI.post(user, %{"status" => "cofe"}) - # Stats should count users with missing or nil `info.deactivated` value - - {:ok, _user} = - user.id - |> User.get_cached_by_id() - |> User.update_info(&Ecto.Changeset.change(&1, %{deactivated: nil})) - Pleroma.Stats.force_update() conn = get(conn, "/api/v1/instance") diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs new file mode 100644 index 000000000..1fcad873d --- /dev/null +++ b/test/web/mastodon_api/controllers/marker_controller_test.exs @@ -0,0 +1,124 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + describe "GET /api/v1/markers" do + test "gets markers with correct scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["read:statuses"]) + + {:ok, %{"notifications" => marker}} = + Pleroma.Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "69420"}} + ) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> get("/api/v1/markers", %{timeline: ["notifications"]}) + |> json_response(200) + + assert response == %{ + "notifications" => %{ + "last_read_id" => "69420", + "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), + "version" => 0 + } + } + end + + test "gets markers with missed scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: []) + + Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => "69420"}}) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> get("/api/v1/markers", %{timeline: ["notifications"]}) + |> json_response(403) + + assert response == %{"error" => "Insufficient permissions: read:statuses."} + end + end + + describe "POST /api/v1/markers" do + test "creates a marker with correct scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69420"} + }) + |> json_response(200) + + assert %{ + "notifications" => %{ + "last_read_id" => "69420", + "updated_at" => _, + "version" => 0 + } + } = response + end + + test "updates exist marker", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["write:statuses"]) + + {:ok, %{"notifications" => marker}} = + Pleroma.Marker.upsert( + user, + %{"notifications" => %{"last_read_id" => "69477"}} + ) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69888"} + }) + |> json_response(200) + + assert response == %{ + "notifications" => %{ + "last_read_id" => "69888", + "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), + "version" => 0 + } + } + end + + test "creates a marker with missed scopes", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: []) + + response = + conn + |> assign(:user, user) + |> assign(:token, token) + |> post("/api/v1/markers", %{ + home: %{last_read_id: "777"}, + notifications: %{"last_read_id" => "69420"} + }) + |> json_response(403) + + assert response == %{"error" => "Insufficient permissions: write:statuses."} + end + end +end diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index e4137e92c..fa55a7cf9 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -137,6 +137,57 @@ 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) + + {: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, unlisted_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "unlisted"}) + + {:ok, private_activity} = + CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "private"}) + + conn = assign(conn, :user, user) + + 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 + + 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 + + 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 + + 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 + end + test "filters notifications using exclude_types", %{conn: conn} do user = insert(:user) other_user = insert(:user) diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 0ca896e01..7953fad62 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -40,9 +40,9 @@ test "it returns empty result if user or status search return undefined error", test "search", %{conn: conn} do user = insert(:user) user_two = insert(:user, %{nickname: "shp@shitposter.club"}) - user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu 天子"}) + user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) - {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu private"}) + {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu private 天子"}) {:ok, _activity} = CommonAPI.post(user, %{ @@ -70,8 +70,8 @@ test "search", %{conn: conn} do get(conn, "/api/v2/search", %{"q" => "天子"}) |> json_response(200) - [account] == results["accounts"] - assert account["id"] == to_string(user_three.id) + [status] = results["statuses"] + assert status["id"] == to_string(activity.id) end end @@ -204,17 +204,17 @@ test "search fetches remote accounts", %{conn: conn} do conn = conn |> assign(:user, user) - |> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "true"}) + |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"}) assert results = json_response(conn, 200) [account] = results["accounts"] - assert account["acct"] == "shp@social.heldscal.la" + assert account["acct"] == "mike@osada.macgirvin.com" end test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do conn = conn - |> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "false"}) + |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "false"}) assert results = json_response(conn, 200) assert [] == results["accounts"] diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index 2de2725e0..a96fd860b 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -12,12 +12,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do alias Pleroma.Object alias Pleroma.Repo alias Pleroma.ScheduledActivity + alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI import Pleroma.Factory + clear_config([:instance, :federating]) + clear_config([:instance, :allow_relay]) + describe "posting statuses" do setup do user = insert(:user) @@ -29,6 +33,34 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do [conn: conn] end + test "posting a status does not increment reblog_count when relaying", %{conn: conn} do + Pleroma.Config.put([:instance, :federating], true) + Pleroma.Config.get([:instance, :allow_relay], true) + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("api/v1/statuses", %{ + "content_type" => "text/plain", + "source" => "Pleroma FE", + "status" => "Hello world", + "visibility" => "public" + }) + |> json_response(200) + + assert response["reblogs_count"] == 0 + ObanHelpers.perform_all() + + response = + conn + |> assign(:user, user) + |> get("api/v1/statuses/#{response["id"]}", %{}) + |> json_response(200) + + assert response["reblogs_count"] == 0 + end + test "posting a status", %{conn: conn} do idempotency_key = "Pikachu rocks!" @@ -526,8 +558,8 @@ test "when you didn't create it", %{conn: conn} do test "when you're an admin or moderator", %{conn: conn} do activity1 = insert(:note_activity) activity2 = insert(:note_activity) - admin = insert(:user, info: %{is_admin: true}) - moderator = insert(:user, info: %{is_moderator: true}) + admin = insert(:user, is_admin: true) + moderator = insert(:user, is_moderator: true) res_conn = conn diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index d3652d964..61b6cea75 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -11,7 +11,6 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do alias Pleroma.Config alias Pleroma.User alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OStatus clear_config([:instance, :public]) @@ -20,27 +19,52 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do :ok end - test "the home timeline", %{conn: conn} do - user = insert(:user) - following = insert(:user) + describe "home" do + test "the home timeline", %{conn: conn} do + user = insert(:user) + following = insert(:user) - {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) + {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/timelines/home") + conn = + conn + |> assign(:user, user) + |> get("/api/v1/timelines/home") - assert Enum.empty?(json_response(conn, :ok)) + assert Enum.empty?(json_response(conn, :ok)) - {:ok, user} = User.follow(user, following) + {:ok, user} = User.follow(user, following) - conn = - build_conn() - |> assign(:user, user) - |> get("/api/v1/timelines/home") + conn = + build_conn() + |> assign(:user, user) + |> get("/api/v1/timelines/home") - assert [%{"content" => "test"}] = json_response(conn, :ok) + assert [%{"content" => "test"}] = json_response(conn, :ok) + end + + test "the home timeline when the direct messages are excluded", %{conn: conn} do + user = insert(:user) + {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"}) + {:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) + + {:ok, unlisted_activity} = + CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"}) + + {:ok, private_activity} = + CommonAPI.post(user, %{"status" => ".", "visibility" => "private"}) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/timelines/home", %{"exclude_visibilities" => ["direct"]}) + + assert status_ids = json_response(conn, :ok) |> Enum.map(& &1["id"]) + assert public_activity.id in status_ids + assert unlisted_activity.id in status_ids + assert private_activity.id in status_ids + refute direct_activity.id in status_ids + end end describe "public" do @@ -50,8 +74,7 @@ test "the public timeline", %{conn: conn} do {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) - {:ok, [_activity]} = - OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") + _activity = insert(:note_activity, local: false) conn = get(conn, "/api/v1/timelines/public", %{"local" => "False"}) @@ -246,9 +269,6 @@ test "hashtag timeline", %{conn: conn} do {:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"}) - {:ok, [_activity]} = - OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") - nconn = get(conn, "/api/v1/timelines/tag/2hu") assert [%{"id" => id}] = json_response(nconn, :ok) diff --git a/test/web/mastodon_api/mastodon_api_test.exs b/test/web/mastodon_api/mastodon_api_test.exs index 7fcb2bd55..561ef05aa 100644 --- a/test/web/mastodon_api/mastodon_api_test.exs +++ b/test/web/mastodon_api/mastodon_api_test.exs @@ -14,11 +14,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPITest do import Pleroma.Factory describe "follow/3" do - test "returns error when user deactivated" do + test "returns error when followed user is deactivated" do follower = insert(:user) - user = insert(:user, local: true, info: %{deactivated: true}) + user = insert(:user, local: true, deactivated: true) {:error, error} = MastodonAPI.follow(follower, user) - assert error == "Could not follow user: You are deactivated." + assert error == "Could not follow user: #{user.nickname} is deactivated." end test "following for user" do diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index b7a4938a6..af88841ed 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -26,12 +26,10 @@ test "Represent a user account" do user = insert(:user, %{ - info: %{ - note_count: 5, - follower_count: 3, - source_data: source_data, - background: background_image - }, + follower_count: 3, + note_count: 5, + source_data: source_data, + background: background_image, nickname: "shp@shitposter.club", name: ":karjalanpiirakka: shp", bio: "valid html", @@ -101,7 +99,7 @@ test "Represent the user account for the account owner" do "non_followers" => true } - privacy = user.info.default_scope + privacy = user.default_scope assert %{ pleroma: %{notification_settings: ^notification_settings}, @@ -112,7 +110,9 @@ test "Represent the user account for the account owner" do test "Represent a Service(bot) account" do user = insert(:user, %{ - info: %{note_count: 5, follower_count: 3, source_data: %{"type" => "Service"}}, + follower_count: 3, + note_count: 5, + source_data: %{"type" => "Service"}, nickname: "shp@shitposter.club", inserted_at: ~N[2017-08-15 15:47:06.597036] }) @@ -164,8 +164,8 @@ test "Represent a Service(bot) account" do end test "Represent a deactivated user for an admin" do - admin = insert(:user, %{info: %{is_admin: true}}) - deactivated_user = insert(:user, %{info: %{deactivated: true}}) + admin = insert(:user, is_admin: true) + deactivated_user = insert(:user, deactivated: true) represented = AccountView.render("show.json", %{user: deactivated_user, for: admin}) assert represented[:pleroma][:deactivated] == true end @@ -253,7 +253,7 @@ test "represent a relationship for the user blocking a domain" do test "represent a relationship for the user with a pending follow request" do user = insert(:user) - other_user = insert(:user, %{info: %User.Info{locked: true}}) + other_user = insert(:user, locked: true) {:ok, user, other_user, _} = CommonAPI.follow(user, other_user) user = User.get_cached_by_id(user.id) @@ -282,7 +282,9 @@ test "represent a relationship for the user with a pending follow request" do test "represent an embedded relationship" do user = insert(:user, %{ - info: %{note_count: 5, follower_count: 0, source_data: %{"type" => "Service"}}, + follower_count: 0, + note_count: 5, + source_data: %{"type" => "Service"}, nickname: "shp@shitposter.club", inserted_at: ~N[2017-08-15 15:47:06.597036] }) @@ -352,7 +354,7 @@ test "represent an embedded relationship" do end test "returns the settings store if the requesting user is the represented user and it's requested specifically" do - user = insert(:user, %{info: %User.Info{pleroma_settings_store: %{fe: "test"}}}) + user = insert(:user, pleroma_settings_store: %{fe: "test"}) result = AccountView.render("show.json", %{user: user, for: user, with_pleroma_settings: true}) @@ -374,14 +376,13 @@ test "sanitizes display names" do describe "hiding follows/following" do test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do - info = %{ - hide_followers: true, - hide_followers_count: true, - hide_follows: true, - hide_follows_count: true - } - - user = insert(:user, info: info) + user = + insert(:user, %{ + hide_followers: true, + hide_followers_count: true, + hide_follows: true, + hide_follows_count: true + }) other_user = insert(:user) {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) @@ -395,7 +396,7 @@ test "shows when follows/followers stats are hidden and sets follow/follower cou end test "shows when follows/followers are hidden" do - user = insert(:user, info: %{hide_followers: true, hide_follows: true}) + user = insert(:user, hide_followers: true, hide_follows: true) other_user = insert(:user) {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) @@ -408,7 +409,7 @@ test "shows when follows/followers are hidden" do end test "shows actual follower/following count to the account owner" do - user = insert(:user, info: %{hide_followers: true, hide_follows: true}) + user = insert(:user, hide_followers: true, hide_follows: true) other_user = insert(:user) {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) @@ -424,8 +425,8 @@ test "shows unread_conversation_count only to the account owner" do other_user = insert(:user) {:ok, _activity} = - CommonAPI.post(user, %{ - "status" => "Hey @#{other_user.nickname}.", + CommonAPI.post(other_user, %{ + "status" => "Hey @#{user.nickname}.", "visibility" => "direct" }) @@ -456,7 +457,7 @@ test "shows zero when no follow requests are pending" do end test "shows non-zero when follow requests are pending" do - user = insert(:user, %{info: %{locked: true}}) + user = insert(:user, locked: true) assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) @@ -468,7 +469,7 @@ test "shows non-zero when follow requests are pending" do end test "decreases when accepting a follow request" do - user = insert(:user, %{info: %{locked: true}}) + user = insert(:user, locked: true) assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) @@ -485,7 +486,7 @@ test "decreases when accepting a follow request" do end test "decreases when rejecting a follow request" do - user = insert(:user, %{info: %{locked: true}}) + user = insert(:user, locked: true) assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) @@ -502,14 +503,14 @@ test "decreases when rejecting a follow request" do end test "shows non-zero when historical unapproved requests are present" do - user = insert(:user, %{info: %{locked: true}}) + user = insert(:user, locked: true) assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user}) other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) - {:ok, user} = User.update_info(user, &User.Info.user_upgrade(&1, %{locked: false})) + {:ok, user} = User.update_and_set_cache(user, %{locked: false}) assert %{locked: false, follow_requests_count: 1} = AccountView.render("show.json", %{user: user, for: user}) diff --git a/test/web/mastodon_api/views/conversation_view_test.exs b/test/web/mastodon_api/views/conversation_view_test.exs index a2a880705..6ed22597d 100644 --- a/test/web/mastodon_api/views/conversation_view_test.exs +++ b/test/web/mastodon_api/views/conversation_view_test.exs @@ -30,5 +30,6 @@ test "represents a Mastodon Conversation entity" do assert [account] = conversation.accounts assert account.id == other_user.id + assert conversation.last_status.pleroma.direct_conversation_id == participation.id end end diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs new file mode 100644 index 000000000..8a5c89d56 --- /dev/null +++ b/test/web/mastodon_api/views/marker_view_test.exs @@ -0,0 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do + use Pleroma.DataCase + alias Pleroma.Web.MastodonAPI.MarkerView + import Pleroma.Factory + + test "returns markers" do + marker1 = insert(:marker, timeline: "notifications", last_read_id: "17") + marker2 = insert(:marker, timeline: "home", last_read_id: "42") + + assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{ + "home" => %{ + last_read_id: "42", + updated_at: NaiveDateTime.to_iso8601(marker2.updated_at), + version: 0 + }, + "notifications" => %{ + last_read_id: "17", + updated_at: NaiveDateTime.to_iso8601(marker1.updated_at), + version: 0 + } + } + 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 1d5a6e956..d46ecc646 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -7,6 +7,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do alias Pleroma.Activity alias Pleroma.Bookmark + alias Pleroma.Conversation.Participation + alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -14,7 +16,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.OStatus import Pleroma.Factory import Tesla.Mock @@ -23,10 +24,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do :ok end - test "returns the direct conversation id when given the `with_conversation_id` option" do + test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) + [participation] = Participation.for_user(user) status = StatusView.render("show.json", @@ -35,7 +37,26 @@ test "returns the direct conversation id when given the `with_conversation_id` o for: user ) - assert status[:pleroma][:direct_conversation_id] + assert status[:pleroma][:direct_conversation_id] == participation.id + + status = StatusView.render("show.json", activity: activity, for: user) + assert status[:pleroma][:direct_conversation_id] == nil + end + + test "returns the direct conversation id when given the `direct_conversation_id` option" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) + [participation] = Participation.for_user(user) + + status = + StatusView.render("show.json", + activity: activity, + direct_conversation_id: participation.id, + for: user + ) + + assert status[:pleroma][:direct_conversation_id] == participation.id end test "returns a temporary ap_id based user for activities missing db users" do @@ -108,7 +129,7 @@ test "a note activity" do in_reply_to_account_id: nil, card: nil, reblog: nil, - content: HtmlSanitizeEx.basic_html(object_data["content"]), + content: HTML.filter_tags(object_data["content"]), created_at: created_at, reblogs_count: 0, replies_count: 0, @@ -120,7 +141,7 @@ test "a note activity" do pinned: false, sensitive: false, poll: nil, - spoiler_text: HtmlSanitizeEx.basic_html(object_data["summary"]), + spoiler_text: HTML.filter_tags(object_data["summary"]), visibility: "public", media_attachments: [], mentions: [], @@ -147,8 +168,8 @@ test "a note activity" do local: true, conversation_id: convo_id, in_reply_to_account_acct: nil, - content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])}, - spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])}, + content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, + spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, expires_at: nil, direct_conversation_id: nil, thread_muted: false @@ -230,17 +251,15 @@ test "a reply" do end test "contains mentions" do - incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml") - # a user with this ap id might be in the cache. - recipient = "https://pleroma.soykaf.com/users/lain" - user = insert(:user, %{ap_id: recipient}) + user = insert(:user) + mentioned = insert(:user) - {:ok, [activity]} = OStatus.handle_incoming(incoming) + {:ok, activity} = CommonAPI.post(user, %{"status" => "hi @#{mentioned.nickname}"}) status = StatusView.render("show.json", %{activity: activity}) assert status.mentions == - Enum.map([user], fn u -> AccountView.render("mention.json", %{user: u}) end) + Enum.map([mentioned], fn u -> AccountView.render("mention.json", %{user: u}) end) end test "create mentions from the 'to' field" do diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs index e15a0bfff..a3281b25b 100644 --- a/test/web/node_info_test.exs +++ b/test/web/node_info_test.exs @@ -24,8 +24,8 @@ test "GET /.well-known/nodeinfo", %{conn: conn} do end test "nodeinfo shows staff accounts", %{conn: conn} do - moderator = insert(:user, %{local: true, info: %{is_moderator: true}}) - admin = insert(:user, %{local: true, info: %{is_admin: true}}) + moderator = insert(:user, local: true, is_moderator: true) + admin = insert(:user, local: true, is_admin: true) conn = conn diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index 41aaf6189..ad8d79083 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -780,8 +780,8 @@ test "rejects token exchange for valid credentials belonging to unconfirmed user {:ok, user} = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) - |> User.change_info(&User.Info.confirmation_changeset(&1, need_confirmation: true)) - |> Repo.update() + |> User.confirmation_changeset(need_confirmation: true) + |> User.update_and_set_cache() refute Pleroma.User.auth_active?(user) @@ -808,7 +808,7 @@ test "rejects token exchange for valid credentials belonging to deactivated user user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password), - info: %{deactivated: true} + deactivated: true ) app = insert(:oauth_app) @@ -834,7 +834,7 @@ test "rejects token exchange for user with password_reset_pending set to true" d user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password), - info: %{password_reset_pending: true} + password_reset_pending: true ) app = insert(:oauth_app, scopes: ["read", "write"]) diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs deleted file mode 100644 index a8d500890..000000000 --- a/test/web/ostatus/activity_representer_test.exs +++ /dev/null @@ -1,300 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.ActivityRepresenterTest do - use Pleroma.DataCase - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.OStatus - alias Pleroma.Web.OStatus.ActivityRepresenter - - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "an external note activity" do - incoming = File.read!("test/fixtures/mastodon-note-cw.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - - user = User.get_cached_by_ap_id(activity.data["actor"]) - - tuple = ActivityRepresenter.to_simple_form(activity, user) - - res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary() - - assert String.contains?( - res, - ~s{} - ) - end - - test "a note activity" do - note_activity = insert(:note_activity) - object_data = Object.normalize(note_activity).data - - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - expected = """ - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - #{object_data["id"]} - New note by #{user.nickname} - #{object_data["content"]} - #{object_data["published"]} - #{object_data["published"]} - #{note_activity.data["context"]} - - #{object_data["summary"]} - - - - - - """ - - tuple = ActivityRepresenter.to_simple_form(note_activity, user) - - res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary() - - assert clean(res) == clean(expected) - end - - test "a reply note" do - user = insert(:user) - note_object = insert(:note) - _note = insert(:note_activity, %{note: note_object}) - object = insert(:note, %{data: %{"inReplyTo" => note_object.data["id"]}}) - answer = insert(:note_activity, %{note: object}) - - Repo.update!( - Object.change(note_object, %{data: Map.put(note_object.data, "external_url", "someurl")}) - ) - - expected = """ - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - #{object.data["id"]} - New note by #{user.nickname} - #{object.data["content"]} - #{object.data["published"]} - #{object.data["published"]} - #{answer.data["context"]} - - 2hu - - - - - - - """ - - tuple = ActivityRepresenter.to_simple_form(answer, user) - - res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary() - - assert clean(res) == clean(expected) - end - - test "an announce activity" do - note = insert(:note_activity) - user = insert(:user) - object = Object.normalize(note) - - {:ok, announce, _object} = ActivityPub.announce(user, object) - - announce = Activity.get_by_id(announce.id) - - note_user = User.get_cached_by_ap_id(note.data["actor"]) - note = Activity.get_by_id(note.id) - - note_xml = - ActivityRepresenter.to_simple_form(note, note_user, true) - |> :xmerl.export_simple_content(:xmerl_xml) - |> to_string - - expected = """ - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - #{announce.data["id"]} - #{user.nickname} repeated a notice - RT #{object.data["content"]} - #{announce.data["published"]} - #{announce.data["published"]} - #{announce.data["context"]} - - - - #{note_xml} - - - - """ - - announce_xml = - ActivityRepresenter.to_simple_form(announce, user) - |> :xmerl.export_simple_content(:xmerl_xml) - |> to_string - - assert clean(expected) == clean(announce_xml) - end - - test "a like activity" do - note = insert(:note) - user = insert(:user) - {:ok, like, _note} = ActivityPub.like(user, note) - - tuple = ActivityRepresenter.to_simple_form(like, user) - refute is_nil(tuple) - - res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary() - - expected = """ - http://activitystrea.ms/schema/1.0/favorite - #{like.data["id"]} - New favorite by #{user.nickname} - #{user.nickname} favorited something - #{like.data["published"]} - #{like.data["published"]} - - http://activitystrea.ms/schema/1.0/note - #{note.data["id"]} - - #{like.data["context"]} - - - - - - """ - - assert clean(res) == clean(expected) - end - - test "a follow activity" do - follower = insert(:user) - followed = insert(:user) - - {:ok, activity} = - ActivityPub.insert(%{ - "type" => "Follow", - "actor" => follower.ap_id, - "object" => followed.ap_id, - "to" => [followed.ap_id] - }) - - tuple = ActivityRepresenter.to_simple_form(activity, follower) - - refute is_nil(tuple) - - res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary() - - expected = """ - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/follow - #{activity.data["id"]} - #{follower.nickname} started following #{activity.data["object"]} - #{follower.nickname} started following #{activity.data["object"]} - #{activity.data["published"]} - #{activity.data["published"]} - - http://activitystrea.ms/schema/1.0/person - #{activity.data["object"]} - #{activity.data["object"]} - - - - """ - - assert clean(res) == clean(expected) - end - - test "an unfollow activity" do - follower = insert(:user) - followed = insert(:user) - {:ok, _activity} = ActivityPub.follow(follower, followed) - {:ok, activity} = ActivityPub.unfollow(follower, followed) - - tuple = ActivityRepresenter.to_simple_form(activity, follower) - - refute is_nil(tuple) - - res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary() - - expected = """ - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/unfollow - #{activity.data["id"]} - #{follower.nickname} stopped following #{followed.ap_id} - #{follower.nickname} stopped following #{followed.ap_id} - #{activity.data["published"]} - #{activity.data["published"]} - - http://activitystrea.ms/schema/1.0/person - #{followed.ap_id} - #{followed.ap_id} - - - - """ - - assert clean(res) == clean(expected) - end - - test "a delete" do - user = insert(:user) - - activity = %Activity{ - data: %{ - "id" => "ap_id", - "type" => "Delete", - "actor" => user.ap_id, - "object" => "some_id", - "published" => "2017-06-18T12:00:18+00:00" - } - } - - tuple = ActivityRepresenter.to_simple_form(activity, nil) - - refute is_nil(tuple) - - res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary() - - expected = """ - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/delete - #{activity.data["object"]} - An object was deleted - An object was deleted - #{activity.data["published"]} - #{activity.data["published"]} - """ - - assert clean(res) == clean(expected) - end - - test "an unknown activity" do - tuple = ActivityRepresenter.to_simple_form(%Activity{}, nil) - assert is_nil(tuple) - end - - defp clean(string) do - String.replace(string, ~r/\s/, "") - end -end diff --git a/test/web/ostatus/feed_representer_test.exs b/test/web/ostatus/feed_representer_test.exs deleted file mode 100644 index d1cadf1e4..000000000 --- a/test/web/ostatus/feed_representer_test.exs +++ /dev/null @@ -1,59 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.FeedRepresenterTest do - use Pleroma.DataCase - import Pleroma.Factory - alias Pleroma.User - alias Pleroma.Web.OStatus - alias Pleroma.Web.OStatus.ActivityRepresenter - alias Pleroma.Web.OStatus.FeedRepresenter - alias Pleroma.Web.OStatus.UserRepresenter - - test "returns a feed of the last 20 items of the user" do - note_activity = insert(:note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - tuple = FeedRepresenter.to_simple_form(user, [note_activity], [user]) - - most_recent_update = - note_activity.updated_at - |> NaiveDateTime.to_iso8601() - - res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> to_string - - user_xml = - UserRepresenter.to_simple_form(user) - |> :xmerl.export_simple_content(:xmerl_xml) - - entry_xml = - ActivityRepresenter.to_simple_form(note_activity, user) - |> :xmerl.export_simple_content(:xmerl_xml) - - expected = """ - - #{OStatus.feed_path(user)} - #{user.nickname}'s timeline - #{most_recent_update} - #{User.avatar_url(user)} - - - - - #{user_xml} - - - - #{entry_xml} - - - """ - - assert clean(res) == clean(expected) - end - - defp clean(string) do - String.replace(string, ~r/\s/, "") - end -end diff --git a/test/web/ostatus/incoming_documents/delete_handling_test.exs b/test/web/ostatus/incoming_documents/delete_handling_test.exs deleted file mode 100644 index cd0447af7..000000000 --- a/test/web/ostatus/incoming_documents/delete_handling_test.exs +++ /dev/null @@ -1,48 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.DeleteHandlingTest do - use Pleroma.DataCase - - import Pleroma.Factory - import Tesla.Mock - - alias Pleroma.Activity - alias Pleroma.Object - alias Pleroma.Web.OStatus - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - describe "deletions" do - test "it removes the mentioned activity" do - note = insert(:note_activity) - second_note = insert(:note_activity) - object = Object.normalize(note) - second_object = Object.normalize(second_note) - user = insert(:user) - - {:ok, like, _object} = Pleroma.Web.ActivityPub.ActivityPub.like(user, object) - - incoming = - File.read!("test/fixtures/delete.xml") - |> String.replace( - "tag:mastodon.sdf.org,2017-06-10:objectId=310513:objectType=Status", - object.data["id"] - ) - - {:ok, [delete]} = OStatus.handle_incoming(incoming) - - refute Activity.get_by_id(note.id) - refute Activity.get_by_id(like.id) - assert Object.get_by_ap_id(object.data["id"]).data["type"] == "Tombstone" - assert Activity.get_by_id(second_note.id) - assert Object.get_by_ap_id(second_object.data["id"]) - - assert delete.data["type"] == "Delete" - end - end -end diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs index b1af918d8..37b7b62f5 100644 --- a/test/web/ostatus/ostatus_controller_test.exs +++ b/test/web/ostatus/ostatus_controller_test.exs @@ -5,13 +5,11 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do use Pleroma.Web.ConnCase - import ExUnit.CaptureLog import Pleroma.Factory alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OStatus.ActivityRepresenter setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -22,78 +20,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do Pleroma.Config.put([:instance, :federating], true) end - describe "salmon_incoming" do - test "decodes a salmon", %{conn: conn} do - user = insert(:user) - salmon = File.read!("test/fixtures/salmon.xml") - - assert capture_log(fn -> - conn = - conn - |> put_req_header("content-type", "application/atom+xml") - |> post("/users/#{user.nickname}/salmon", salmon) - - assert response(conn, 200) - end) =~ "[error]" - end - - test "decodes a salmon with a changed magic key", %{conn: conn} do - user = insert(:user) - salmon = File.read!("test/fixtures/salmon.xml") - - assert capture_log(fn -> - conn = - conn - |> put_req_header("content-type", "application/atom+xml") - |> post("/users/#{user.nickname}/salmon", salmon) - - assert response(conn, 200) - end) =~ "[error]" - - # Wrong key - info = %{ - magic_key: - "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwrong1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" - } - - # Set a wrong magic-key for a user so it has to refetch - "http://gs.example.org:4040/index.php/user/1" - |> User.get_cached_by_ap_id() - |> User.update_info(&User.Info.remote_user_creation(&1, info)) - - assert capture_log(fn -> - conn = - build_conn() - |> put_req_header("content-type", "application/atom+xml") - |> post("/users/#{user.nickname}/salmon", salmon) - - assert response(conn, 200) - end) =~ "[error]" - end - end - describe "GET object/2" do - test "gets an object", %{conn: conn} do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) - url = "/objects/#{uuid}" - - conn = - conn - |> put_req_header("accept", "application/xml") - |> get(url) - - expected = - ActivityRepresenter.to_simple_form(note_activity, user, true) - |> ActivityRepresenter.wrap_with_entry() - |> :xmerl.export_simple(:xmerl_xml) - |> to_string - - assert response(conn, 200) == expected - end - test "redirects to /notice/id for html format", %{conn: conn} do note_activity = insert(:note_activity) object = Object.normalize(note_activity) @@ -143,16 +70,6 @@ test "404s on nonexisting objects", %{conn: conn} do end describe "GET activity/2" do - test "gets an activity in xml format", %{conn: conn} do - note_activity = insert(:note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - - conn - |> put_req_header("accept", "application/xml") - |> get("/activities/#{uuid}") - |> response(200) - end - test "redirects to /notice/id for html format", %{conn: conn} do note_activity = insert(:note_activity) [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) @@ -180,24 +97,6 @@ test "505s when user not found", %{conn: conn} do assert response(conn, 500) == ~S({"error":"Something went wrong"}) end - test "404s on deleted objects", %{conn: conn} do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) - - conn - |> put_req_header("accept", "application/xml") - |> get("/objects/#{uuid}") - |> response(200) - - Object.delete(object) - - conn - |> put_req_header("accept", "application/xml") - |> get("/objects/#{uuid}") - |> response(404) - end - test "404s on private activities", %{conn: conn} do note_activity = insert(:direct_note_activity) [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs deleted file mode 100644 index 70a0e4473..000000000 --- a/test/web/ostatus/ostatus_test.exs +++ /dev/null @@ -1,645 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatusTest do - use Pleroma.DataCase - alias Pleroma.Activity - alias Pleroma.Instances - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.OStatus - alias Pleroma.Web.XML - - import ExUnit.CaptureLog - import Mock - import Pleroma.Factory - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "don't insert create notes twice" do - incoming = File.read!("test/fixtures/incoming_note_activity.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - assert {:ok, [activity]} == OStatus.handle_incoming(incoming) - end - - test "handle incoming note - GS, Salmon" do - incoming = File.read!("test/fixtures/incoming_note_activity.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - object = Object.normalize(activity) - - user = User.get_cached_by_ap_id(activity.data["actor"]) - assert user.info.note_count == 1 - assert activity.data["type"] == "Create" - assert object.data["type"] == "Note" - - assert object.data["id"] == "tag:gs.example.org:4040,2017-04-23:noticeId=29:objectType=note" - - assert activity.data["published"] == "2017-04-23T14:51:03+00:00" - assert object.data["published"] == "2017-04-23T14:51:03+00:00" - - assert activity.data["context"] == - "tag:gs.example.org:4040,2017-04-23:objectType=thread:nonce=f09e22f58abd5c7b" - - assert "http://pleroma.example.org:4000/users/lain3" in activity.data["to"] - assert object.data["emoji"] == %{"marko" => "marko.png", "reimu" => "reimu.png"} - assert activity.local == false - end - - test "handle incoming notes - GS, subscription" do - incoming = File.read!("test/fixtures/ostatus_incoming_post.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - object = Object.normalize(activity) - - assert activity.data["type"] == "Create" - assert object.data["type"] == "Note" - assert object.data["actor"] == "https://social.heldscal.la/user/23211" - assert object.data["content"] == "Will it blend?" - user = User.get_cached_by_ap_id(activity.data["actor"]) - assert User.ap_followers(user) in activity.data["to"] - assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"] - end - - test "handle incoming notes with attachments - GS, subscription" do - incoming = File.read!("test/fixtures/incoming_websub_gnusocial_attachments.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - object = Object.normalize(activity) - - assert activity.data["type"] == "Create" - assert object.data["type"] == "Note" - assert object.data["actor"] == "https://social.heldscal.la/user/23211" - assert object.data["attachment"] |> length == 2 - assert object.data["external_url"] == "https://social.heldscal.la/notice/2020923" - assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"] - end - - test "handle incoming notes with tags" do - incoming = File.read!("test/fixtures/ostatus_incoming_post_tag.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - object = Object.normalize(activity) - - assert object.data["tag"] == ["nsfw"] - assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"] - end - - test "handle incoming notes - Mastodon, salmon, reply" do - # It uses the context of the replied to object - Repo.insert!(%Object{ - data: %{ - "id" => "https://pleroma.soykaf.com/objects/c237d966-ac75-4fe3-a87a-d89d71a3a7a4", - "context" => "2hu" - } - }) - - incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - object = Object.normalize(activity) - - assert activity.data["type"] == "Create" - assert object.data["type"] == "Note" - assert object.data["actor"] == "https://mastodon.social/users/lambadalambda" - assert activity.data["context"] == "2hu" - assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"] - end - - test "handle incoming notes - Mastodon, with CW" do - incoming = File.read!("test/fixtures/mastodon-note-cw.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - object = Object.normalize(activity) - - assert activity.data["type"] == "Create" - assert object.data["type"] == "Note" - assert object.data["actor"] == "https://mastodon.social/users/lambadalambda" - assert object.data["summary"] == "technologic" - assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"] - end - - test "handle incoming unlisted messages, put public into cc" do - incoming = File.read!("test/fixtures/mastodon-note-unlisted.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - object = Object.normalize(activity) - - refute "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"] - assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["cc"] - refute "https://www.w3.org/ns/activitystreams#Public" in object.data["to"] - assert "https://www.w3.org/ns/activitystreams#Public" in object.data["cc"] - end - - test "handle incoming retweets - Mastodon, with CW" do - incoming = File.read!("test/fixtures/cw_retweet.xml") - {:ok, [[_activity, retweeted_activity]]} = OStatus.handle_incoming(incoming) - retweeted_object = Object.normalize(retweeted_activity) - - assert retweeted_object.data["summary"] == "Hey." - end - - test "handle incoming notes - GS, subscription, reply" do - incoming = File.read!("test/fixtures/ostatus_incoming_reply.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - object = Object.normalize(activity) - - assert activity.data["type"] == "Create" - assert object.data["type"] == "Note" - assert object.data["actor"] == "https://social.heldscal.la/user/23211" - - assert object.data["content"] == - "@shpbot why not indeed." - - assert object.data["inReplyTo"] == - "tag:gs.archae.me,2017-04-30:noticeId=778260:objectType=note" - - assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"] - end - - test "handle incoming retweets - GS, subscription" do - incoming = File.read!("test/fixtures/share-gs.xml") - {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming) - - assert activity.data["type"] == "Announce" - assert activity.data["actor"] == "https://social.heldscal.la/user/23211" - assert activity.data["object"] == retweeted_activity.data["object"] - assert "https://pleroma.soykaf.com/users/lain" in activity.data["to"] - refute activity.local - - retweeted_activity = Activity.get_by_id(retweeted_activity.id) - retweeted_object = Object.normalize(retweeted_activity) - assert retweeted_activity.data["type"] == "Create" - assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain" - refute retweeted_activity.local - assert retweeted_object.data["announcement_count"] == 1 - assert String.contains?(retweeted_object.data["content"], "mastodon") - refute String.contains?(retweeted_object.data["content"], "Test account") - end - - test "handle incoming retweets - GS, subscription - local message" do - incoming = File.read!("test/fixtures/share-gs-local.xml") - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - - incoming = - incoming - |> String.replace("LOCAL_ID", object.data["id"]) - |> String.replace("LOCAL_USER", user.ap_id) - - {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming) - - assert activity.data["type"] == "Announce" - assert activity.data["actor"] == "https://social.heldscal.la/user/23211" - assert activity.data["object"] == object.data["id"] - assert user.ap_id in activity.data["to"] - refute activity.local - - retweeted_activity = Activity.get_by_id(retweeted_activity.id) - assert note_activity.id == retweeted_activity.id - assert retweeted_activity.data["type"] == "Create" - assert retweeted_activity.data["actor"] == user.ap_id - assert retweeted_activity.local - assert Object.normalize(retweeted_activity).data["announcement_count"] == 1 - end - - test "handle incoming retweets - Mastodon, salmon" do - incoming = File.read!("test/fixtures/share.xml") - {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming) - retweeted_object = Object.normalize(retweeted_activity) - - assert activity.data["type"] == "Announce" - assert activity.data["actor"] == "https://mastodon.social/users/lambadalambda" - assert activity.data["object"] == retweeted_activity.data["object"] - - assert activity.data["id"] == - "tag:mastodon.social,2017-05-03:objectId=4934452:objectType=Status" - - refute activity.local - assert retweeted_activity.data["type"] == "Create" - assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain" - refute retweeted_activity.local - refute String.contains?(retweeted_object.data["content"], "Test account") - end - - test "handle incoming favorites - GS, websub" do - capture_log(fn -> - incoming = File.read!("test/fixtures/favorite.xml") - {:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming) - - assert activity.data["type"] == "Like" - assert activity.data["actor"] == "https://social.heldscal.la/user/23211" - assert activity.data["object"] == favorited_activity.data["object"] - - assert activity.data["id"] == - "tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061643:2017-05-05T09:12:50+00:00" - - refute activity.local - assert favorited_activity.data["type"] == "Create" - assert favorited_activity.data["actor"] == "https://shitposter.club/user/1" - - assert favorited_activity.data["object"] == - "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" - - refute favorited_activity.local - end) - end - - test "handle conversation references" do - incoming = File.read!("test/fixtures/mastodon_conversation.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - - assert activity.data["context"] == - "tag:mastodon.social,2017-08-28:objectId=7876885:objectType=Conversation" - end - - test "handle incoming favorites with locally available object - GS, websub" do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - - incoming = - File.read!("test/fixtures/favorite_with_local_note.xml") - |> String.replace("localid", object.data["id"]) - - {:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming) - - assert activity.data["type"] == "Like" - assert activity.data["actor"] == "https://social.heldscal.la/user/23211" - assert activity.data["object"] == object.data["id"] - refute activity.local - assert note_activity.id == favorited_activity.id - assert favorited_activity.local - end - - test_with_mock "handle incoming replies, fetching replied-to activities if we don't have them", - OStatus, - [:passthrough], - [] do - incoming = File.read!("test/fixtures/incoming_note_activity_answer.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - object = Object.normalize(activity, false) - - assert activity.data["type"] == "Create" - assert object.data["type"] == "Note" - - assert object.data["inReplyTo"] == - "http://pleroma.example.org:4000/objects/55bce8fc-b423-46b1-af71-3759ab4670bc" - - assert "http://pleroma.example.org:4000/users/lain5" in activity.data["to"] - - assert object.data["id"] == "tag:gs.example.org:4040,2017-04-25:noticeId=55:objectType=note" - - assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"] - - assert called(OStatus.fetch_activity_from_url(object.data["inReplyTo"], :_)) - end - - test_with_mock "handle incoming replies, not fetching replied-to activities beyond max_replies_depth", - OStatus, - [:passthrough], - [] do - incoming = File.read!("test/fixtures/incoming_note_activity_answer.xml") - - with_mock Pleroma.Web.Federator, - allowed_incoming_reply_depth?: fn _ -> false end do - {:ok, [activity]} = OStatus.handle_incoming(incoming) - object = Object.normalize(activity, false) - - refute called(OStatus.fetch_activity_from_url(object.data["inReplyTo"], :_)) - end - end - - test "handle incoming follows" do - incoming = File.read!("test/fixtures/follow.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - assert activity.data["type"] == "Follow" - - assert activity.data["id"] == - "tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00" - - assert activity.data["actor"] == "https://social.heldscal.la/user/23211" - assert activity.data["object"] == "https://pawoo.net/users/pekorino" - refute activity.local - - follower = User.get_cached_by_ap_id(activity.data["actor"]) - followed = User.get_cached_by_ap_id(activity.data["object"]) - - assert User.following?(follower, followed) - end - - test "refuse following over OStatus if the followed's account is locked" do - incoming = File.read!("test/fixtures/follow.xml") - _user = insert(:user, info: %{locked: true}, ap_id: "https://pawoo.net/users/pekorino") - - {:ok, [{:error, "It's not possible to follow locked accounts over OStatus"}]} = - OStatus.handle_incoming(incoming) - end - - test "handle incoming unfollows with existing follow" do - incoming_follow = File.read!("test/fixtures/follow.xml") - {:ok, [_activity]} = OStatus.handle_incoming(incoming_follow) - - incoming = File.read!("test/fixtures/unfollow.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - - assert activity.data["type"] == "Undo" - - assert activity.data["id"] == - "undo:tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00" - - assert activity.data["actor"] == "https://social.heldscal.la/user/23211" - embedded_object = activity.data["object"] - assert is_map(embedded_object) - assert embedded_object["type"] == "Follow" - assert embedded_object["object"] == "https://pawoo.net/users/pekorino" - refute activity.local - - follower = User.get_cached_by_ap_id(activity.data["actor"]) - followed = User.get_cached_by_ap_id(embedded_object["object"]) - - refute User.following?(follower, followed) - end - - test "it clears `unreachable` federation status of the sender" do - incoming_reaction_xml = File.read!("test/fixtures/share-gs.xml") - doc = XML.parse_document(incoming_reaction_xml) - actor_uri = XML.string_from_xpath("//author/uri[1]", doc) - reacted_to_author_uri = XML.string_from_xpath("//author/uri[2]", doc) - - Instances.set_consistently_unreachable(actor_uri) - Instances.set_consistently_unreachable(reacted_to_author_uri) - refute Instances.reachable?(actor_uri) - refute Instances.reachable?(reacted_to_author_uri) - - {:ok, _} = OStatus.handle_incoming(incoming_reaction_xml) - assert Instances.reachable?(actor_uri) - refute Instances.reachable?(reacted_to_author_uri) - end - - describe "new remote user creation" do - test "returns local users" do - local_user = insert(:user) - {:ok, user} = OStatus.find_or_make_user(local_user.ap_id) - - assert user == local_user - end - - test "tries to use the information in poco fields" do - uri = "https://social.heldscal.la/user/23211" - - {:ok, user} = OStatus.find_or_make_user(uri) - - user = User.get_cached_by_id(user.id) - assert user.name == "Constance Variable" - assert user.nickname == "lambadalambda@social.heldscal.la" - assert user.local == false - assert user.info.uri == uri - assert user.ap_id == uri - assert user.bio == "Call me Deacon Blues." - assert user.avatar["type"] == "Image" - - {:ok, user_again} = OStatus.find_or_make_user(uri) - - assert user == user_again - end - - test "find_or_make_user sets all the nessary input fields" do - uri = "https://social.heldscal.la/user/23211" - {:ok, user} = OStatus.find_or_make_user(uri) - - assert user.info == - %User.Info{ - id: user.info.id, - ap_enabled: false, - background: %{}, - banner: %{}, - blocks: [], - deactivated: false, - default_scope: "public", - domain_blocks: [], - follower_count: 0, - is_admin: false, - is_moderator: false, - keys: nil, - locked: false, - no_rich_text: false, - note_count: 0, - settings: nil, - source_data: %{}, - hub: "https://social.heldscal.la/main/push/hub", - magic_key: - "RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB", - salmon: "https://social.heldscal.la/main/salmon/user/23211", - topic: "https://social.heldscal.la/api/statuses/user_timeline/23211.atom", - uri: "https://social.heldscal.la/user/23211" - } - end - - test "find_make_or_update_actor takes an author element and returns an updated user" do - uri = "https://social.heldscal.la/user/23211" - - {:ok, user} = OStatus.find_or_make_user(uri) - old_name = user.name - old_bio = user.bio - change = Ecto.Changeset.change(user, %{avatar: nil, bio: nil, name: nil}) - - {:ok, user} = Repo.update(change) - refute user.avatar - - doc = XML.parse_document(File.read!("test/fixtures/23211.atom")) - [author] = :xmerl_xpath.string('//author[1]', doc) - {:ok, user} = OStatus.find_make_or_update_actor(author) - assert user.avatar["type"] == "Image" - assert user.name == old_name - assert user.bio == old_bio - - {:ok, user_again} = OStatus.find_make_or_update_actor(author) - assert user_again == user - end - - test "find_or_make_user disallows protocol downgrade" do - user = insert(:user, %{local: true}) - {:ok, user} = OStatus.find_or_make_user(user.ap_id) - - assert User.ap_enabled?(user) - - user = - insert(:user, %{ - ap_id: "https://social.heldscal.la/user/23211", - info: %{ap_enabled: true}, - local: false - }) - - assert User.ap_enabled?(user) - - {:ok, user} = OStatus.find_or_make_user(user.ap_id) - assert User.ap_enabled?(user) - end - - test "find_make_or_update_actor disallows protocol downgrade" do - user = insert(:user, %{local: true}) - {:ok, user} = OStatus.find_or_make_user(user.ap_id) - - assert User.ap_enabled?(user) - - user = - insert(:user, %{ - ap_id: "https://social.heldscal.la/user/23211", - info: %{ap_enabled: true}, - local: false - }) - - assert User.ap_enabled?(user) - - {:ok, user} = OStatus.find_or_make_user(user.ap_id) - assert User.ap_enabled?(user) - - doc = XML.parse_document(File.read!("test/fixtures/23211.atom")) - [author] = :xmerl_xpath.string('//author[1]', doc) - {:error, :invalid_protocol} = OStatus.find_make_or_update_actor(author) - end - end - - describe "gathering user info from a user id" do - test "it returns user info in a hash" do - user = "shp@social.heldscal.la" - - # TODO: make test local - {:ok, data} = OStatus.gather_user_info(user) - - expected = %{ - "hub" => "https://social.heldscal.la/main/push/hub", - "magic_key" => - "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB", - "name" => "shp", - "nickname" => "shp", - "salmon" => "https://social.heldscal.la/main/salmon/user/29191", - "subject" => "acct:shp@social.heldscal.la", - "topic" => "https://social.heldscal.la/api/statuses/user_timeline/29191.atom", - "uri" => "https://social.heldscal.la/user/29191", - "host" => "social.heldscal.la", - "fqn" => user, - "bio" => "cofe", - "avatar" => %{ - "type" => "Image", - "url" => [ - %{ - "href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", - "mediaType" => "image/jpeg", - "type" => "Link" - } - ] - }, - "subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}", - "ap_id" => nil - } - - assert data == expected - end - - test "it works with the uri" do - user = "https://social.heldscal.la/user/29191" - - # TODO: make test local - {:ok, data} = OStatus.gather_user_info(user) - - expected = %{ - "hub" => "https://social.heldscal.la/main/push/hub", - "magic_key" => - "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB", - "name" => "shp", - "nickname" => "shp", - "salmon" => "https://social.heldscal.la/main/salmon/user/29191", - "subject" => "https://social.heldscal.la/user/29191", - "topic" => "https://social.heldscal.la/api/statuses/user_timeline/29191.atom", - "uri" => "https://social.heldscal.la/user/29191", - "host" => "social.heldscal.la", - "fqn" => user, - "bio" => "cofe", - "avatar" => %{ - "type" => "Image", - "url" => [ - %{ - "href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", - "mediaType" => "image/jpeg", - "type" => "Link" - } - ] - }, - "subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}", - "ap_id" => nil - } - - assert data == expected - end - end - - describe "fetching a status by it's HTML url" do - test "it builds a missing status from an html url" do - capture_log(fn -> - url = "https://shitposter.club/notice/2827873" - {:ok, [activity]} = OStatus.fetch_activity_from_url(url) - - assert activity.data["actor"] == "https://shitposter.club/user/1" - - assert activity.data["object"] == - "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" - end) - end - - test "it works for atom notes, too" do - url = "https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056" - {:ok, [activity]} = OStatus.fetch_activity_from_url(url) - assert activity.data["actor"] == "https://social.sakamoto.gq/users/eal" - assert activity.data["object"] == url - end - end - - test "it doesn't add nil in the to field" do - incoming = File.read!("test/fixtures/nil_mention_entry.xml") - {:ok, [activity]} = OStatus.handle_incoming(incoming) - - assert activity.data["to"] == [ - "http://localhost:4001/users/atarifrosch@social.stopwatchingus-heidelberg.de/followers", - "https://www.w3.org/ns/activitystreams#Public" - ] - end - - describe "is_representable?" do - test "Note objects are representable" do - note_activity = insert(:note_activity) - - assert OStatus.is_representable?(note_activity) - end - - test "Article objects are not representable" do - note_activity = insert(:note_activity) - note_object = Object.normalize(note_activity) - - note_data = - note_object.data - |> Map.put("type", "Article") - - Cachex.clear(:object_cache) - - cs = Object.change(note_object, %{data: note_data}) - {:ok, _article_object} = Repo.update(cs) - - # the underlying object is now an Article instead of a note, so this should fail - refute OStatus.is_representable?(note_activity) - end - end - - describe "make_user/2" do - test "creates new user" do - {:ok, user} = OStatus.make_user("https://social.heldscal.la/user/23211") - - created_user = - User - |> Repo.get_by(ap_id: "https://social.heldscal.la/user/23211") - |> Map.put(:last_digest_emailed_at, nil) - - assert user.info - assert user == created_user - end - end -end diff --git a/test/web/ostatus/user_representer_test.exs b/test/web/ostatus/user_representer_test.exs deleted file mode 100644 index e3863d2e9..000000000 --- a/test/web/ostatus/user_representer_test.exs +++ /dev/null @@ -1,38 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OStatus.UserRepresenterTest do - use Pleroma.DataCase - alias Pleroma.Web.OStatus.UserRepresenter - - import Pleroma.Factory - alias Pleroma.User - - test "returns a user with id, uri, name and link" do - user = insert(:user, %{nickname: "レイン"}) - tuple = UserRepresenter.to_simple_form(user) - - res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> to_string - - expected = """ - #{user.ap_id} - http://activitystrea.ms/schema/1.0/person - #{user.ap_id} - #{user.nickname} - #{user.name} - #{user.bio} - #{user.bio} - #{user.nickname} - - - true - """ - - assert clean(res) == clean(expected) - end - - defp clean(string) do - String.replace(string, ~r/\s/, "") - end -end diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs index 3b4665afd..c809f510f 100644 --- a/test/web/pleroma_api/controllers/account_controller_test.exs +++ b/test/web/pleroma_api/controllers/account_controller_test.exs @@ -6,7 +6,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do use Pleroma.Web.ConnCase alias Pleroma.Config - alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.CommonAPI @@ -20,10 +19,10 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do setup do {:ok, user} = insert(:user) - |> User.change_info(&User.Info.confirmation_changeset(&1, need_confirmation: true)) - |> Repo.update() + |> User.confirmation_changeset(need_confirmation: true) + |> User.update_and_set_cache() - assert user.info.confirmation_pending + assert user.confirmation_pending [user: user] end @@ -105,7 +104,7 @@ test "can set profile banner", %{conn: conn} do |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => @image}) user = refresh_record(user) - assert user.info.banner["type"] == "Image" + assert user.banner["type"] == "Image" assert %{"url" => _} = json_response(conn, 200) end @@ -119,7 +118,7 @@ test "can reset profile banner", %{conn: conn} do |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => ""}) user = refresh_record(user) - assert user.info.banner == %{} + assert user.banner == %{} assert %{"url" => nil} = json_response(conn, 200) end @@ -135,7 +134,7 @@ test "background image can be set", %{conn: conn} do |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => @image}) user = refresh_record(user) - assert user.info.background["type"] == "Image" + assert user.background["type"] == "Image" assert %{"url" => _} = json_response(conn, 200) end @@ -148,14 +147,14 @@ test "background image can be reset", %{conn: conn} do |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => ""}) user = refresh_record(user) - assert user.info.background == %{} + assert user.background == %{} assert %{"url" => nil} = json_response(conn, 200) end end describe "getting favorites timeline of specified user" do setup do - [current_user, user] = insert_pair(:user, %{info: %{hide_favorites: false}}) + [current_user, user] = insert_pair(:user, hide_favorites: false) [current_user: current_user, user: user] end @@ -319,7 +318,7 @@ test "returns 403 error when user has hidden own favorites", %{ conn: conn, current_user: current_user } do - user = insert(:user, %{info: %{hide_favorites: true}}) + user = insert(:user, hide_favorites: true) activity = insert(:note_activity) CommonAPI.favorite(activity.id, user) @@ -341,7 +340,7 @@ test "hides favorites for new users by default", %{conn: conn, current_user: cur |> assign(:user, current_user) |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - assert user.info.hide_favorites + assert user.hide_favorites assert json_response(conn, 403) == %{"error" => "Can't get favorites"} end end diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs index 5f74460e8..3d3becefd 100644 --- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs @@ -38,7 +38,7 @@ test "shared & non-shared pack information in list_packs is ok" do end test "listing remote packs" do - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) conn = build_conn() |> assign(:user, admin) resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200) @@ -121,7 +121,7 @@ test "downloading shared & unshared packs from another instance via download_fro text(File.read!("#{@emoji_dir_path}/test_pack_nonshared/nonshared.zip")) end) - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) conn = build_conn() |> assign(:user, admin) @@ -206,7 +206,7 @@ test "downloading shared & unshared packs from another instance via download_fro end) {:ok, - admin: insert(:user, info: %{is_admin: true}), + admin: insert(:user, is_admin: true), pack_file: pack_file, new_data: %{ "license" => "Test license changed", @@ -303,7 +303,7 @@ test "updating pack files" do File.rm_rf!("#{@emoji_dir_path}/test_pack/dir_2") end) - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) conn = build_conn() @@ -391,7 +391,7 @@ test "creating and deleting a pack" do File.rm_rf!("#{@emoji_dir_path}/test_created") end) - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) conn = build_conn() |> assign(:user, admin) @@ -431,7 +431,7 @@ test "filesystem import" do refute Map.has_key?(resp, "test_pack_for_import") - admin = insert(:user, info: %{is_admin: true}) + admin = insert(:user, is_admin: true) assert conn |> assign(:user, admin) diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs index 44ea85b45..b1b59beed 100644 --- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs @@ -155,6 +155,33 @@ test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do assert other_user in participation.recipients end + test "POST /api/v1/pleroma/conversations/read", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}", "visibility" => "direct"}) + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}", "visibility" => "direct"}) + + [participation2, participation1] = Participation.for_user(other_user) + assert Participation.get(participation2.id).read == false + assert Participation.get(participation1.id).read == false + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2 + + [%{"unread" => false}, %{"unread" => false}] = + conn + |> assign(:user, other_user) + |> post("/api/v1/pleroma/conversations/read", %{}) + |> json_response(200) + + [participation2, participation1] = Participation.for_user(other_user) + assert Participation.get(participation2.id).read == true + assert Participation.get(participation1.id).read == true + assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 + end + describe "POST /api/v1/pleroma/notifications/read" do test "it marks a single notification as read", %{conn: conn} do user1 = insert(:user) diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index 2f6ce4bd2..9b554601d 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -84,7 +84,7 @@ test "fail message sending" do ) == :error end - test "delete subsciption if restult send message between 400..500" do + test "delete subscription if result send message between 400..500" do subscription = insert(:push_subscription) assert Impl.push_message( @@ -97,7 +97,7 @@ test "delete subsciption if restult send message between 400..500" do refute Pleroma.Repo.get(Subscription, subscription.id) end - test "renders body for create activity" do + test "renders title and body for create activity" do user = insert(:user, nickname: "Bob") {:ok, activity} = @@ -116,18 +116,24 @@ test "renders body for create activity" do object ) == "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." + + assert Impl.format_title(%{activity: activity}) == + "New Mention" end - test "renders body for follow activity" do + test "renders title and body for follow activity" do user = insert(:user, nickname: "Bob") other_user = insert(:user) {:ok, _, _, activity} = CommonAPI.follow(user, other_user) object = Object.normalize(activity) assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has followed you" + + assert Impl.format_title(%{activity: activity}) == + "New Follower" end - test "renders body for announce activity" do + test "renders title and body for announce activity" do user = insert(:user) {:ok, activity} = @@ -141,9 +147,12 @@ test "renders body for announce activity" do assert Impl.format_body(%{activity: announce_activity}, user, object) == "@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." + + assert Impl.format_title(%{activity: announce_activity}) == + "New Repeat" end - test "renders body for like activity" do + test "renders title and body for like activity" do user = insert(:user, nickname: "Bob") {:ok, activity} = @@ -156,5 +165,21 @@ test "renders body for like activity" do object = Object.normalize(activity) assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post" + + assert Impl.format_title(%{activity: activity}) == + "New Favorite" + end + + test "renders title for create activity with direct visibility" do + user = insert(:user, nickname: "Bob") + + {:ok, activity} = + CommonAPI.post(user, %{ + "visibility" => "direct", + "status" => "This is just between you and me, pal" + }) + + assert Impl.format_title(%{activity: activity}) == + "New Direct Message" end end diff --git a/test/web/rel_me_test.exs b/test/web/rel_me_test.exs index 2251fed16..77b5d5dc6 100644 --- a/test/web/rel_me_test.exs +++ b/test/web/rel_me_test.exs @@ -14,7 +14,9 @@ test "parse/1" do hrefs = ["https://social.example.org/users/lain"] assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/null") == {:ok, []} - assert {:error, _} = Pleroma.Web.RelMe.parse("http://example.com/rel_me/error") + + assert {:ok, %Tesla.Env{status: 404}} = + Pleroma.Web.RelMe.parse("http://example.com/rel_me/error") assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/link") == {:ok, hrefs} assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor") == {:ok, hrefs} diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs deleted file mode 100644 index 153ec41ac..000000000 --- a/test/web/salmon/salmon_test.exs +++ /dev/null @@ -1,101 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Salmon.SalmonTest do - use Pleroma.DataCase - alias Pleroma.Activity - alias Pleroma.Keys - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.Federator.Publisher - alias Pleroma.Web.Salmon - import Mock - import Pleroma.Factory - - @magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" - - @wrong_magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAA" - - @magickey_friendica "RSA.AMwa8FUs2fWEjX0xN7yRQgegQffhBpuKNC6fa5VNSVorFjGZhRrlPMn7TQOeihlc9lBz2OsHlIedbYn2uJ7yCs0.AQAB" - - setup_all do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "decodes a salmon" do - {:ok, salmon} = File.read("test/fixtures/salmon.xml") - {:ok, doc} = Salmon.decode_and_validate(@magickey, salmon) - assert Regex.match?(~r/xml/, doc) - end - - test "errors on wrong magic key" do - {:ok, salmon} = File.read("test/fixtures/salmon.xml") - assert Salmon.decode_and_validate(@wrong_magickey, salmon) == :error - end - - test "it encodes a magic key from a public key" do - key = Salmon.decode_key(@magickey) - magic_key = Salmon.encode_key(key) - - assert @magickey == magic_key - end - - test "it decodes a friendica public key" do - _key = Salmon.decode_key(@magickey_friendica) - end - - test "encodes an xml payload with a private key" do - doc = File.read!("test/fixtures/incoming_note_activity.xml") - pem = File.read!("test/fixtures/private_key.pem") - {:ok, private, public} = Keys.keys_from_pem(pem) - - # Let's try a roundtrip. - {:ok, salmon} = Salmon.encode(private, doc) - {:ok, decoded_doc} = Salmon.decode_and_validate(Salmon.encode_key(public), salmon) - - assert doc == decoded_doc - end - - test "it gets a magic key" do - salmon = File.read!("test/fixtures/salmon2.xml") - {:ok, key} = Salmon.fetch_magic_key(salmon) - - assert key == - "RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB" - end - - test_with_mock "it pushes an activity to remote accounts it's addressed to", - Publisher, - [:passthrough], - [] do - user_data = %{ - info: %{ - salmon: "http://test-example.org/salmon" - }, - local: false - } - - mentioned_user = insert(:user, user_data) - note = insert(:note) - - activity_data = %{ - "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), - "type" => "Create", - "actor" => note.data["actor"], - "to" => note.data["to"] ++ [mentioned_user.ap_id], - "object" => note.data, - "published_at" => DateTime.utc_now() |> DateTime.to_iso8601(), - "context" => note.data["context"] - } - - {:ok, activity} = Repo.insert(%Activity{data: activity_data, recipients: activity_data["to"]}) - user = User.get_cached_by_ap_id(activity.data["actor"]) - {:ok, user} = User.ensure_keys_present(user) - - Salmon.publish(user, activity) - - assert called(Publisher.enqueue_one(Salmon, %{recipient_id: mentioned_user.id})) - end -end diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index d33eb1e42..80a7541b2 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.StreamerTest do import Pleroma.Factory + alias Pleroma.Conversation.Participation alias Pleroma.List alias Pleroma.User alias Pleroma.Web.CommonAPI @@ -110,6 +111,24 @@ test "it doesn't send notify to the 'user:notification' stream' when a domain is Streamer.stream("user:notification", notif) Task.await(task) end + + test "it sends follow activities to the 'user:notification' stream", %{ + user: user + } do + user2 = insert(:user) + task = Task.async(fn -> assert_receive {:text, _}, 4_000 end) + + Streamer.add_socket( + "user:notification", + %{transport_pid: task.pid, assigns: %{user: user}} + ) + + {:ok, _follower, _followed, _activity} = CommonAPI.follow(user2, user) + + # We don't directly pipe the notification to the streamer as it's already + # generated as a side effect of CommonAPI.follow(). + Task.await(task) + end end test "it sends to public" do @@ -169,7 +188,8 @@ test "it sends to public" do test "it doesn't send to user if recipients invalid and thread containment is enabled" do Pleroma.Config.put([:instance, :skip_thread_containment], false) author = insert(:user) - user = insert(:user, following: [author.ap_id]) + user = insert(:user) + User.follow(user, author, "accept") activity = insert(:note_activity, @@ -191,7 +211,8 @@ test "it doesn't send to user if recipients invalid and thread containment is en test "it sends message if recipients invalid and thread containment is disabled" do Pleroma.Config.put([:instance, :skip_thread_containment], true) author = insert(:user) - user = insert(:user, following: [author.ap_id]) + user = insert(:user) + User.follow(user, author, "accept") activity = insert(:note_activity, @@ -213,7 +234,8 @@ test "it sends message if recipients invalid and thread containment is disabled" test "it sends message if recipients invalid and thread containment is enabled but user's thread containment is disabled" do Pleroma.Config.put([:instance, :skip_thread_containment], false) author = insert(:user) - user = insert(:user, following: [author.ap_id], info: %{skip_thread_containment: true}) + user = insert(:user, skip_thread_containment: true) + User.follow(user, author, "accept") activity = insert(:note_activity, @@ -460,7 +482,14 @@ test "it sends conversation update to the 'direct' stream", %{} do task = Task.async(fn -> - assert_receive {:text, _received_event}, 4_000 + assert_receive {:text, received_event}, 4_000 + + assert %{"event" => "conversation", "payload" => received_payload} = + Jason.decode!(received_event) + + assert %{"last_status" => last_status} = Jason.decode!(received_payload) + [participation] = Participation.for_user(user) + assert last_status["pleroma"]["direct_conversation_id"] == participation.id end) Streamer.add_socket( @@ -477,7 +506,7 @@ test "it sends conversation update to the 'direct' stream", %{} do Task.await(task) end - test "it doesn't send conversation update to the 'direct' streamj when the last message in the conversation is deleted" do + test "it doesn't send conversation update to the 'direct' stream when the last message in the conversation is deleted" do user = insert(:user) another_user = insert(:user) diff --git a/test/web/twitter_api/password_controller_test.exs b/test/web/twitter_api/password_controller_test.exs index dc6d4e3e3..840c84a05 100644 --- a/test/web/twitter_api/password_controller_test.exs +++ b/test/web/twitter_api/password_controller_test.exs @@ -59,7 +59,7 @@ test "it returns HTTP 200", %{conn: conn} do end test "it sets password_reset_pending to false", %{conn: conn} do - user = insert(:user, info: %{password_reset_pending: true}) + user = insert(:user, password_reset_pending: true) {:ok, token} = PasswordResetToken.create_token(user) {:ok, _access_token} = Token.create_token(insert(:oauth_app), user, %{}) @@ -75,7 +75,7 @@ test "it sets password_reset_pending to false", %{conn: conn} do |> post("/api/pleroma/password_reset", %{data: params}) |> html_response(:ok) - assert User.get_by_id(user.id).info.password_reset_pending == false + assert User.get_by_id(user.id).password_reset_pending == false end end end diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs index d1d61d11a..85a9be3e0 100644 --- a/test/web/twitter_api/twitter_api_test.exs +++ b/test/web/twitter_api/twitter_api_test.exs @@ -71,7 +71,7 @@ test "it sends confirmation email if :account_activation_required is specified i {:ok, user} = TwitterAPI.register_user(data) ObanHelpers.perform_all() - assert user.info.confirmation_pending + assert user.confirmation_pending email = Pleroma.Emails.UserEmail.account_confirmation_email(user) diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 9d4cb70f0..f0211f59c 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -164,7 +164,7 @@ test "it updates notification settings", %{conn: conn} do "follows" => true, "non_follows" => true, "non_followers" => true - } == user.info.notification_settings + } == user.notification_settings end end @@ -366,11 +366,11 @@ test "follows user", %{conn: conn} do |> response(200) assert response =~ "Account followed!" - assert user2.follower_address in refresh_record(user).following + assert user2.follower_address in User.following(user) end test "returns error when user is deactivated", %{conn: conn} do - user = insert(:user, info: %{deactivated: true}) + user = insert(:user, deactivated: true) user2 = insert(:user) response = @@ -438,7 +438,7 @@ test "follows", %{conn: conn} do |> response(200) assert response =~ "Account followed!" - assert user2.follower_address in refresh_record(user).following + assert user2.follower_address in User.following(user) end test "returns error when followee not found", %{conn: conn} do @@ -568,7 +568,7 @@ test "it returns HTTP 200", %{conn: conn} do user = User.get_cached_by_id(user.id) - assert user.info.deactivated == true + assert user.deactivated == true end test "it returns returns when password invalid", %{conn: conn} do @@ -583,7 +583,7 @@ test "it returns returns when password invalid", %{conn: conn} do assert response == %{"error" => "Invalid password."} user = User.get_cached_by_id(user.id) - refute user.info.deactivated + refute user.deactivated end end diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs index 696c1bd70..5aa8c73cf 100644 --- a/test/web/web_finger/web_finger_test.exs +++ b/test/web/web_finger/web_finger_test.exs @@ -45,19 +45,6 @@ test "returns error when fails parse xml or json" do assert {:error, %Jason.DecodeError{}} = WebFinger.finger(user) end - test "returns the info for an OStatus user" do - user = "shp@social.heldscal.la" - - {:ok, data} = WebFinger.finger(user) - - assert data["magic_key"] == - "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB" - - assert data["topic"] == "https://social.heldscal.la/api/statuses/user_timeline/29191.atom" - assert data["subject"] == "acct:shp@social.heldscal.la" - assert data["salmon"] == "https://social.heldscal.la/main/salmon/user/29191" - end - test "returns the ActivityPub actor URI for an ActivityPub user" do user = "framasoft@framatube.org" @@ -72,20 +59,6 @@ test "returns the ActivityPub actor URI for an ActivityPub user with the ld+json assert data["ap_id"] == "https://gerzilla.de/channel/kaniini" end - test "returns the correctly for json ostatus users" do - user = "winterdienst@gnusocial.de" - - {:ok, data} = WebFinger.finger(user) - - assert data["magic_key"] == - "RSA.qfYaxztz7ZELrE4v5WpJrPM99SKI3iv9Y3Tw6nfLGk-4CRljNYqV8IYX2FXjeucC_DKhPNnlF6fXyASpcSmA_qupX9WC66eVhFhZ5OuyBOeLvJ1C4x7Hi7Di8MNBxY3VdQuQR0tTaS_YAZCwASKp7H6XEid3EJpGt0EQZoNzRd8=.AQAB" - - assert data["topic"] == "https://gnusocial.de/api/statuses/user_timeline/249296.atom" - assert data["subject"] == "acct:winterdienst@gnusocial.de" - assert data["salmon"] == "https://gnusocial.de/main/salmon/user/249296" - assert data["subscribe_address"] == "https://gnusocial.de/main/ostatussub?profile={uri}" - end - test "it work for AP-only user" do user = "kpherox@mstdn.jp" diff --git a/test/web/websub/websub_controller_test.exs b/test/web/websub/websub_controller_test.exs deleted file mode 100644 index f6d002b3b..000000000 --- a/test/web/websub/websub_controller_test.exs +++ /dev/null @@ -1,86 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Websub.WebsubControllerTest do - use Pleroma.Web.ConnCase - import Pleroma.Factory - alias Pleroma.Repo - alias Pleroma.Web.Websub - alias Pleroma.Web.Websub.WebsubClientSubscription - - clear_config_all([:instance, :federating]) do - Pleroma.Config.put([:instance, :federating], true) - end - - test "websub subscription request", %{conn: conn} do - user = insert(:user) - - path = Pleroma.Web.OStatus.pubsub_path(user) - - data = %{ - "hub.callback": "http://example.org/sub", - "hub.mode": "subscribe", - "hub.topic": Pleroma.Web.OStatus.feed_path(user), - "hub.secret": "a random secret", - "hub.lease_seconds": "100" - } - - conn = - conn - |> post(path, data) - - assert response(conn, 202) == "Accepted" - end - - test "websub subscription confirmation", %{conn: conn} do - websub = insert(:websub_client_subscription) - - params = %{ - "hub.mode" => "subscribe", - "hub.topic" => websub.topic, - "hub.challenge" => "some challenge", - "hub.lease_seconds" => "100" - } - - conn = - conn - |> get("/push/subscriptions/#{websub.id}", params) - - websub = Repo.get(WebsubClientSubscription, websub.id) - - assert response(conn, 200) == "some challenge" - assert websub.state == "accepted" - assert_in_delta NaiveDateTime.diff(websub.valid_until, NaiveDateTime.utc_now()), 100, 5 - end - - describe "websub_incoming" do - test "accepts incoming feed updates", %{conn: conn} do - websub = insert(:websub_client_subscription) - doc = "some stuff" - signature = Websub.sign(websub.secret, doc) - - conn = - conn - |> put_req_header("x-hub-signature", "sha1=" <> signature) - |> put_req_header("content-type", "application/atom+xml") - |> post("/push/subscriptions/#{websub.id}", doc) - - assert response(conn, 200) == "OK" - end - - test "rejects incoming feed updates with the wrong signature", %{conn: conn} do - websub = insert(:websub_client_subscription) - doc = "some stuff" - signature = Websub.sign("wrong secret", doc) - - conn = - conn - |> put_req_header("x-hub-signature", "sha1=" <> signature) - |> put_req_header("content-type", "application/atom+xml") - |> post("/push/subscriptions/#{websub.id}", doc) - - assert response(conn, 500) == "Error" - end - end -end diff --git a/test/web/websub/websub_test.exs b/test/web/websub/websub_test.exs deleted file mode 100644 index 46ca545de..000000000 --- a/test/web/websub/websub_test.exs +++ /dev/null @@ -1,236 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.WebsubTest do - use Pleroma.DataCase - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.Router.Helpers - alias Pleroma.Web.Websub - alias Pleroma.Web.Websub.WebsubClientSubscription - alias Pleroma.Web.Websub.WebsubServerSubscription - alias Pleroma.Workers.SubscriberWorker - - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - test "a verification of a request that is accepted" do - sub = insert(:websub_subscription) - topic = sub.topic - - getter = fn _path, _headers, options -> - %{ - "hub.challenge": challenge, - "hub.lease_seconds": seconds, - "hub.topic": ^topic, - "hub.mode": "subscribe" - } = Keyword.get(options, :params) - - assert String.to_integer(seconds) > 0 - - {:ok, - %Tesla.Env{ - status: 200, - body: challenge - }} - end - - {:ok, sub} = Websub.verify(sub, getter) - assert sub.state == "active" - end - - test "a verification of a request that doesn't return 200" do - sub = insert(:websub_subscription) - - getter = fn _path, _headers, _options -> - {:ok, - %Tesla.Env{ - status: 500, - body: "" - }} - end - - {:error, sub} = Websub.verify(sub, getter) - # Keep the current state. - assert sub.state == "requested" - end - - test "an incoming subscription request" do - user = insert(:user) - - data = %{ - "hub.callback" => "http://example.org/sub", - "hub.mode" => "subscribe", - "hub.topic" => Pleroma.Web.OStatus.feed_path(user), - "hub.secret" => "a random secret", - "hub.lease_seconds" => "100" - } - - {:ok, subscription} = Websub.incoming_subscription_request(user, data) - assert subscription.topic == Pleroma.Web.OStatus.feed_path(user) - assert subscription.state == "requested" - assert subscription.secret == "a random secret" - assert subscription.callback == "http://example.org/sub" - end - - test "an incoming subscription request for an existing subscription" do - user = insert(:user) - - sub = - insert(:websub_subscription, state: "accepted", topic: Pleroma.Web.OStatus.feed_path(user)) - - data = %{ - "hub.callback" => sub.callback, - "hub.mode" => "subscribe", - "hub.topic" => Pleroma.Web.OStatus.feed_path(user), - "hub.secret" => "a random secret", - "hub.lease_seconds" => "100" - } - - {:ok, subscription} = Websub.incoming_subscription_request(user, data) - assert subscription.topic == Pleroma.Web.OStatus.feed_path(user) - assert subscription.state == sub.state - assert subscription.secret == "a random secret" - assert subscription.callback == sub.callback - assert length(Repo.all(WebsubServerSubscription)) == 1 - assert subscription.id == sub.id - end - - def accepting_verifier(subscription) do - {:ok, %{subscription | state: "accepted"}} - end - - test "initiate a subscription for a given user and topic" do - subscriber = insert(:user) - user = insert(:user, %{info: %Pleroma.User.Info{topic: "some_topic", hub: "some_hub"}}) - - {:ok, websub} = Websub.subscribe(subscriber, user, &accepting_verifier/1) - assert websub.subscribers == [subscriber.ap_id] - assert websub.topic == "some_topic" - assert websub.hub == "some_hub" - assert is_binary(websub.secret) - assert websub.user == user - assert websub.state == "accepted" - end - - test "discovers the hub and canonical url" do - topic = "https://mastodon.social/users/lambadalambda.atom" - - {:ok, discovered} = Websub.gather_feed_data(topic) - - expected = %{ - "hub" => "https://mastodon.social/api/push", - "uri" => "https://mastodon.social/users/lambadalambda", - "nickname" => "lambadalambda", - "name" => "Critical Value", - "host" => "mastodon.social", - "bio" => "a cool dude.", - "avatar" => %{ - "type" => "Image", - "url" => [ - %{ - "href" => - "https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif?1492379244", - "mediaType" => "image/gif", - "type" => "Link" - } - ] - } - } - - assert expected == discovered - end - - test "calls the hub, requests topic" do - hub = "https://social.heldscal.la/main/push/hub" - topic = "https://social.heldscal.la/api/statuses/user_timeline/23211.atom" - websub = insert(:websub_client_subscription, %{hub: hub, topic: topic}) - - poster = fn ^hub, {:form, data}, _headers -> - assert Keyword.get(data, :"hub.mode") == "subscribe" - - assert Keyword.get(data, :"hub.callback") == - Helpers.websub_url( - Pleroma.Web.Endpoint, - :websub_subscription_confirmation, - websub.id - ) - - {:ok, %{status: 202}} - end - - task = Task.async(fn -> Websub.request_subscription(websub, poster) end) - - change = Ecto.Changeset.change(websub, %{state: "accepted"}) - {:ok, _} = Repo.update(change) - - {:ok, websub} = Task.await(task) - - assert websub.state == "accepted" - end - - test "rejects the subscription if it can't be accepted" do - hub = "https://social.heldscal.la/main/push/hub" - topic = "https://social.heldscal.la/api/statuses/user_timeline/23211.atom" - websub = insert(:websub_client_subscription, %{hub: hub, topic: topic}) - - poster = fn ^hub, {:form, _data}, _headers -> - {:ok, %{status: 202}} - end - - {:error, websub} = Websub.request_subscription(websub, poster, 1000) - assert websub.state == "rejected" - - websub = insert(:websub_client_subscription, %{hub: hub, topic: topic}) - - poster = fn ^hub, {:form, _data}, _headers -> - {:ok, %{status: 400}} - end - - {:error, websub} = Websub.request_subscription(websub, poster, 1000) - assert websub.state == "rejected" - end - - test "sign a text" do - signed = Websub.sign("secret", "text") - assert signed == "B8392C23690CCF871F37EC270BE1582DEC57A503" |> String.downcase() - - _signed = Websub.sign("secret", [["て"], ['す']]) - end - - describe "renewing subscriptions" do - test "it renews subscriptions that have less than a day of time left" do - day = 60 * 60 * 24 - now = NaiveDateTime.utc_now() - - still_good = - insert(:websub_client_subscription, %{ - valid_until: NaiveDateTime.add(now, 2 * day), - topic: "http://example.org/still_good", - hub: "http://example.org/still_good", - state: "accepted" - }) - - needs_refresh = - insert(:websub_client_subscription, %{ - valid_until: NaiveDateTime.add(now, day - 100), - topic: "http://example.org/needs_refresh", - hub: "http://example.org/needs_refresh", - state: "accepted" - }) - - _refresh = Websub.refresh_subscriptions() - ObanHelpers.perform(all_enqueued(worker: SubscriberWorker)) - - assert still_good == Repo.get(WebsubClientSubscription, still_good.id) - refute needs_refresh == Repo.get(WebsubClientSubscription, needs_refresh.id) - end - end -end