Browse Source

Merge branch 'develop' into feature/push-subject-for-dm

merge-requests/1875/head
Mark Felder 4 years ago
parent
commit
dcb23a85b6
88 changed files with 1694 additions and 4220 deletions
  1. +31
    -0
      .gitlab-ci.yml
  2. +79
    -21
      CHANGELOG.md
  3. +229
    -0
      benchmarks/load_testing/fetcher.ex
  4. +352
    -0
      benchmarks/load_testing/generator.ex
  5. +11
    -0
      benchmarks/load_testing/helper.ex
  6. +134
    -0
      benchmarks/mix/tasks/pleroma/load_testing.ex
  7. +84
    -0
      config/benchmark.exs
  8. +1
    -7
      config/config.exs
  9. +1
    -3
      config/description.exs
  10. +1
    -1
      config/releases.exs
  11. +81
    -6
      docs/API/admin_api.md
  12. +2
    -2
      lib/mix/tasks/pleroma/database.ex
  13. +3
    -7
      lib/mix/tasks/pleroma/relay.ex
  14. +0
    -10
      lib/pleroma/application.ex
  15. +6
    -0
      lib/pleroma/conversation/participation.ex
  16. +32
    -32
      lib/pleroma/moderation_log.ex
  17. +2
    -2
      lib/pleroma/object.ex
  18. +19
    -10
      lib/pleroma/object/containment.ex
  19. +14
    -14
      lib/pleroma/object/fetcher.ex
  20. +1
    -32
      lib/pleroma/upload.ex
  21. +24
    -33
      lib/pleroma/user.ex
  22. +0
    -6
      lib/pleroma/user/info.ex
  23. +63
    -103
      lib/pleroma/user/search.ex
  24. +26
    -8
      lib/pleroma/web/activity_pub/activity_pub.ex
  25. +1
    -1
      lib/pleroma/web/activity_pub/publisher.ex
  26. +14
    -0
      lib/pleroma/web/activity_pub/relay.ex
  27. +0
    -15
      lib/pleroma/web/activity_pub/transmogrifier.ex
  28. +114
    -24
      lib/pleroma/web/admin_api/admin_api_controller.ex
  29. +6
    -0
      lib/pleroma/web/admin_api/views/account_view.ex
  30. +0
    -52
      lib/pleroma/web/federator/federator.ex
  31. +26
    -0
      lib/pleroma/web/federator/publisher.ex
  32. +7
    -0
      lib/pleroma/web/mastodon_api/websocket_handler.ex
  33. +0
    -313
      lib/pleroma/web/ostatus/activity_representer.ex
  34. +0
    -66
      lib/pleroma/web/ostatus/feed_representer.ex
  35. +0
    -18
      lib/pleroma/web/ostatus/handlers/delete_handler.ex
  36. +0
    -26
      lib/pleroma/web/ostatus/handlers/follow_handler.ex
  37. +0
    -168
      lib/pleroma/web/ostatus/handlers/note_handler.ex
  38. +0
    -22
      lib/pleroma/web/ostatus/handlers/unfollow_handler.ex
  39. +0
    -395
      lib/pleroma/web/ostatus/ostatus.ex
  40. +1
    -45
      lib/pleroma/web/ostatus/ostatus_controller.ex
  41. +0
    -41
      lib/pleroma/web/ostatus/user_representer.ex
  42. +11
    -6
      lib/pleroma/web/router.ex
  43. +0
    -254
      lib/pleroma/web/salmon/salmon.ex
  44. +3
    -3
      lib/pleroma/web/streamer/streamer.ex
  45. +0
    -2
      lib/pleroma/web/templates/feed/feed/feed.xml.eex
  46. +0
    -12
      lib/pleroma/web/web_finger/web_finger.ex
  47. +0
    -332
      lib/pleroma/web/websub/websub.ex
  48. +0
    -20
      lib/pleroma/web/websub/websub_client_subscription.ex
  49. +0
    -99
      lib/pleroma/web/websub/websub_controller.ex
  50. +0
    -17
      lib/pleroma/web/websub/websub_server_subscription.ex
  51. +0
    -4
      lib/pleroma/workers/receiver_worker.ex
  52. +0
    -26
      lib/pleroma/workers/subscriber_worker.ex
  53. +1
    -0
      mix.exs
  54. +22
    -0
      priv/repo/migrations/20190711042021_create_safe_jsonb_set.exs
  55. +1
    -1
      priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs
  56. +2
    -2
      rel/files/bin/pleroma_ctl
  57. +34
    -1
      test/conversation/participation_test.exs
  58. +1
    -0
      test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json
  59. +1
    -0
      test/fixtures/tesla_mock/moonman@shitposter.club.json
  60. +4
    -4
      test/moderation_log_test.exs
  61. +1
    -1
      test/object/containment_test.exs
  62. +3
    -36
      test/object/fetcher_test.exs
  63. +12
    -0
      test/safe_jsonb_set_test.exs
  64. +1
    -2
      test/signature_test.exs
  65. +0
    -20
      test/support/factory.ex
  66. +9
    -1
      test/support/http_request_mock.ex
  67. +0
    -26
      test/user_search_test.exs
  68. +0
    -22
      test/user_test.exs
  69. +21
    -0
      test/web/activity_pub/activity_pub_test.exs
  70. +4
    -4
      test/web/activity_pub/relay_test.exs
  71. +0
    -44
      test/web/activity_pub/transmogrifier_test.exs
  72. +178
    -49
      test/web/admin_api/admin_api_controller_test.exs
  73. +0
    -87
      test/web/federator_test.exs
  74. +10
    -4
      test/web/mastodon_api/controllers/conversation_controller_test.exs
  75. +3
    -3
      test/web/mastodon_api/controllers/search_controller_test.exs
  76. +1
    -6
      test/web/mastodon_api/controllers/timeline_controller_test.exs
  77. +2
    -2
      test/web/mastodon_api/views/account_view_test.exs
  78. +4
    -7
      test/web/mastodon_api/views/status_view_test.exs
  79. +0
    -300
      test/web/ostatus/activity_representer_test.exs
  80. +0
    -59
      test/web/ostatus/feed_representer_test.exs
  81. +0
    -48
      test/web/ostatus/incoming_documents/delete_handling_test.exs
  82. +0
    -100
      test/web/ostatus/ostatus_controller_test.exs
  83. +0
    -645
      test/web/ostatus/ostatus_test.exs
  84. +0
    -38
      test/web/ostatus/user_representer_test.exs
  85. +0
    -101
      test/web/salmon/salmon_test.exs
  86. +0
    -27
      test/web/web_finger/web_finger_test.exs
  87. +0
    -86
      test/web/websub/websub_controller_test.exs
  88. +0
    -236
      test/web/websub/websub_test.exs

+ 31
- 0
.gitlab-ci.yml View File

@@ -15,6 +15,7 @@ cache:
stages:
- build
- test
- benchmark
- deploy
- release

@@ -28,6 +29,36 @@ build:
- mix deps.get
- mix compile --force

docs-build:
stage: build
only:
- master@pleroma/pleroma
- develop@pleroma/pleroma
variables:
MIX_ENV: dev
PLEROMA_BUILD_ENV: prod
script:
- mix deps.get
- mix compile
- mix docs
artifacts:
paths:
- priv/static/doc

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:


+ 79
- 21
CHANGELOG.md View File

@@ -4,8 +4,39 @@ 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
<details>
<summary>API Changes</summary>

- **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
</details>

### 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).
<details>
<summary>API Changes</summary>

- 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,10 +45,11 @@ 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

### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
@@ -30,15 +62,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- 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`)
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
</details>

### Fixed
- Report emails now include functional links to profiles of remote user accounts
<details>
<summary>API Changes</summary>

- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
- Added `:instance, extended_nickname_format` setting to the default config
- Report emails now include functional links to profiles of remote user accounts
</details>

## [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.0] - 2019-??-??
**Breaking:** The stable branch has been changed from `master` to `stable`, `master` now points to `release/1.0`
## [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`

@@ -46,16 +100,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`
@@ -64,7 +117,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
@@ -75,21 +128,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.
@@ -103,6 +163,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. <https://github.com/tootsuite/mastodon/pull/11196>
- Mastodon API: Add support for muting/unmuting notifications
- Mastodon API: Add support for the `blocked_by` attribute in the relationship API (`GET /api/v1/accounts/relationships`). <https://github.com/tootsuite/mastodon/pull/10373>
@@ -111,7 +172,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
@@ -129,11 +191,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=<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


+ 229
- 0
benchmarks/load_testing/fetcher.ex View File

@@ -0,0 +1,229 @@
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
}

Benchee.run(%{
"User home timeline" => fn ->
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities(
[user.ap_id | user.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(
[user.ap_id | user.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

+ 352
- 0
benchmarks/load_testing/generator.ex View File

@@ -0,0 +1,352 @@
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",
following: [ap_id]
}
else
%{
ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user),
following: [User.ap_id(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" =>
"<p><span class=\"h-card\"><a href=\"" <>
user.ap_id <>
"\" class=\"u-url mention\">@<span>" <> user.nickname <> "</span></a></span></p>",
"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

+ 11
- 0
benchmarks/load_testing/helper.ex View File

@@ -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

+ 134
- 0
benchmarks/mix/tasks/pleroma/load_testing.ex View File

@@ -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

+ 84
- 0
config/benchmark.exs View File

@@ -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

+ 1
- 7
config/config.exs View File

@@ -59,10 +59,6 @@ scheduled_jobs =
_ -> []
end

scheduled_jobs =
scheduled_jobs ++
[{"0 */6 * * * *", {Pleroma.Web.Websub, :refresh_subscriptions, []}}]

config :pleroma, Pleroma.Scheduler,
global: true,
overlap: true,
@@ -243,9 +239,7 @@ config :pleroma, :instance,
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,


+ 1
- 3
config/description.exs View File

@@ -581,9 +581,7 @@ config :pleroma, :config_description, [
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
]
},
%{


+ 1
- 1
config/releases.exs View File

@@ -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"


+ 81
- 6
docs/API/admin_api.md View File

@@ -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`

## `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.info`

## 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`
- 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.info`
- 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


+ 2
- 2
lib/mix/tasks/pleroma/database.ex View File

@@ -28,7 +28,7 @@ defmodule Mix.Tasks.Pleroma.Database 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
)
@@ -126,7 +126,7 @@ defmodule Mix.Tasks.Pleroma.Database do
set: [
data:
fragment(
"jsonb_set(?, '{likes}', '[]'::jsonb, true)",
"safe_jsonb_set(?, '{likes}', '[]'::jsonb, true)",
object.data
)
]


+ 3
- 7
lib/mix/tasks/pleroma/relay.ex View File

@@ -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 @@ defmodule Mix.Tasks.Pleroma.Relay 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

+ 0
- 10
lib/pleroma/application.ex View File

@@ -161,11 +161,6 @@ defmodule Pleroma.Application 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
@@ -178,11 +173,6 @@ defmodule Pleroma.Application do
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]},
restart: :temporary


+ 6
- 0
lib/pleroma/conversation/participation.ex View File

@@ -48,6 +48,12 @@ defmodule Pleroma.Conversation.Participation 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})


+ 32
- 32
lib/pleroma/moderation_log.ex View File

@@ -86,18 +86,18 @@ defmodule Pleroma.ModerationLog 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 @@ defmodule Pleroma.ModerationLog do
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 @@ defmodule Pleroma.ModerationLog do
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 @@ defmodule Pleroma.ModerationLog do
"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,10 @@ defmodule Pleroma.ModerationLog do
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "activate",
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
"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 +385,10 @@ defmodule Pleroma.ModerationLog do
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "deactivate",
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
"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 +400,9 @@ defmodule Pleroma.ModerationLog do
"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 +414,9 @@ defmodule Pleroma.ModerationLog do
"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 +424,11 @@ defmodule Pleroma.ModerationLog do
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "grant",
"subject" => %{"nickname" => subject_nickname},
"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 +436,11 @@ defmodule Pleroma.ModerationLog do
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "revoke",
"subject" => %{"nickname" => subject_nickname},
"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 +539,16 @@ defmodule Pleroma.ModerationLog do
}) do
"@#{actor_nickname} deleted status ##{subject_id}"
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

+ 2
- 2
lib/pleroma/object.ex View File

@@ -181,7 +181,7 @@ defmodule Pleroma.Object do
data:
fragment(
"""
jsonb_set(?, '{repliesCount}',
safe_jsonb_set(?, '{repliesCount}',
(coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
""",
o.data,
@@ -204,7 +204,7 @@ defmodule Pleroma.Object do
data:
fragment(
"""
jsonb_set(?, '{repliesCount}',
safe_jsonb_set(?, '{repliesCount}',
(greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
""",
o.data,


+ 19
- 10
lib/pleroma/object/containment.ex View File

@@ -32,6 +32,23 @@ defmodule Pleroma.Object.Containment do
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 @@ defmodule Pleroma.Object.Containment 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 @@ defmodule Pleroma.Object.Containment 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}),


+ 14
- 14
lib/pleroma/object/fetcher.ex View File

@@ -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
@@ -67,7 +66,8 @@ defmodule Pleroma.Object.Fetcher 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 +75,12 @@ defmodule Pleroma.Object.Fetcher 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 +90,8 @@ defmodule Pleroma.Object.Fetcher 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...")

# 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 +110,8 @@ defmodule Pleroma.Object.Fetcher do
with {:ok, object} <- fetch_object_from_id(id, options) do
object
else
_e ->
e ->
Logger.error("Error while fetching #{id}: #{inspect(e)}")
nil
end
end
@@ -161,7 +158,7 @@ defmodule Pleroma.Object.Fetcher 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 +167,9 @@ defmodule Pleroma.Object.Fetcher do
{:ok, %{status: code}} when code in [404, 410] ->
{:error, "Object has been deleted"}

{:scheme, _} ->
{:error, "Unsupported URI scheme"}

e ->
{:error, e}
end


+ 1
- 32
lib/pleroma/upload.ex View File

@@ -105,7 +105,7 @@ defmodule Pleroma.Upload 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 @@ defmodule Pleroma.Upload 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


+ 24
- 33
lib/pleroma/user.ex View File

@@ -26,9 +26,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
@@ -437,10 +435,6 @@ defmodule Pleroma.User do
{: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,
@@ -614,12 +608,7 @@ defmodule Pleroma.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
@@ -725,7 +714,7 @@ defmodule Pleroma.User do
set: [
info:
fragment(
"jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
"safe_jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
u.info,
u.info
)
@@ -746,7 +735,7 @@ defmodule Pleroma.User do
set: [
info:
fragment(
"jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
"safe_jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
u.info,
u.info
)
@@ -816,7 +805,7 @@ defmodule Pleroma.User do
set: [
info:
fragment(
"jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
"safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
u.info,
s.count
)
@@ -1059,7 +1048,15 @@ defmodule Pleroma.User do
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
end

def deactivate(%User{} = user, status \\ true) 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} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
Enum.each(get_followers(user), &invalidate_cache/1)
Enum.each(get_friends(user), &update_follower_count/1)
@@ -1072,6 +1069,10 @@ defmodule Pleroma.User do
update_info(user, &User.Info.update_notification_settings(&1, settings))
end

def delete(users) when is_list(users) do
for user <- users, do: delete(user)
end

def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
@@ -1234,18 +1235,7 @@ defmodule Pleroma.User do

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)
@@ -1300,11 +1290,6 @@ defmodule Pleroma.User do
{: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 get_public_key_for_ap_id(ap_id) do
@@ -1625,6 +1610,12 @@ defmodule Pleroma.User do

`fun` is called with the `user.info`.
"""
def update_info(users, fun) when is_list(users) do
Repo.transaction(fn ->
for user <- users, do: update_info(user, fun)
end)
end

def update_info(user, fun) do
user
|> change_info(fun)


+ 0
- 6
lib/pleroma/user/info.ex View File

@@ -39,9 +39,6 @@ defmodule Pleroma.User.Info do
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)
@@ -262,9 +259,6 @@ defmodule Pleroma.User.Info do
:locked,
:magic_key,
:uri,
:hub,
:topic,
:salmon,
:hide_followers,
:hide_follows,
:hide_followers_count,


+ 63
- 103
lib/pleroma/user/search.ex View File

@@ -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 @@ defmodule Pleroma.User.Search 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,15 +46,65 @@ defmodule Pleroma.User.Search 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

@nickname_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~\-@]+$/
defp fts_search(query, query_string) do
{nickname_weight, name_weight} =
if String.match?(query_string, @nickname_regex) do
{"A", "B"}
else
{"B", "A"}
end

query_string = to_tsquery(query_string)

from(
u in query,
where:
fragment(
"""
(setweight(to_tsvector('simple', ?), ?) || setweight(to_tsvector('simple', ?), ?)) @@ to_tsquery('simple', ?)
""",
u.name,
^name_weight,
u.nickname,
^nickname_weight,
^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)

@@ -87,21 +127,6 @@ defmodule Pleroma.User.Search do

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 +151,9 @@ defmodule Pleroma.User.Search do

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 +162,8 @@ defmodule Pleroma.User.Search 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 +179,5 @@ defmodule Pleroma.User.Search 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

+ 26
- 8
lib/pleroma/web/activity_pub/activity_pub.ex View File

@@ -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
@@ -131,7 +132,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{: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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# 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,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
_ <- increase_poll_votes_if_vote(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
{: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}

@@ -1203,7 +1219,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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



+ 1
- 1
lib/pleroma/web/activity_pub/publisher.ex View File

@@ -129,7 +129,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher 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


+ 14
- 0
lib/pleroma/web/activity_pub/relay.ex View File

@@ -51,6 +51,20 @@ defmodule Pleroma.Web.ActivityPub.Relay do

def publish(_), do: {:error, "Not implemented"}

@spec list() :: {:ok, [String.t()]} | {:error, any()}
def list do
with %User{following: following} = _user <- get_actor() do
list =
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


+ 0
- 15
lib/pleroma/web/activity_pub/transmogrifier.ex View File

@@ -1073,8 +1073,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do

Repo.update_all(q, [])

maybe_retire_websub(user.ap_id)

q =
from(
a in Activity,
@@ -1117,19 +1115,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier 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


+ 114
- 24
lib/pleroma/web/admin_api/admin_api_controller.ex View File

@@ -46,6 +46,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:user_delete,
:users_create,
:user_toggle_activation,
:user_activate,
:user_deactivate,
:tag_users,
:untag_users,
:right_add,
@@ -98,7 +100,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do

ModerationLog.insert_log(%{
actor: admin,
subject: user,
subject: [user],
action: "delete"
})

@@ -106,6 +108,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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
@@ -240,7 +256,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do

ModerationLog.insert_log(%{
actor: admin,
subject: user,
subject: [user],
action: action
})

@@ -249,6 +265,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> 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(%{
@@ -313,6 +359,31 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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
info = Map.put(%{}, "is_" <> permission_group, true)

users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)

User.update_info(users, &User.Info.admin_api_update(&1, info))

ModerationLog.insert_log(%{
action: "grant",
actor: admin,
subject: users,
permission: permission_group
})

json(conn, info)
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
@@ -328,7 +399,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
ModerationLog.insert_log(%{
action: "grant",
actor: admin,
subject: user,
subject: [user],
permission: permission_group
})

@@ -349,8 +420,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
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
info = Map.put(%{}, "is_" <> permission_group, false)

users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)

User.update_info(users, &User.Info.admin_api_update(&1, info))

ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
subject: users,
permission: permission_group
})

json(conn, info)
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(
@@ -371,33 +470,24 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
subject: user,
subject: [user],
permission: permission_group
})

json(conn, info)
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



+ 6
- 0
lib/pleroma/web/admin_api/views/account_view.ex View File

@@ -19,6 +19,12 @@ defmodule Pleroma.Web.AdminAPI.AccountView 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)


+ 0
- 52
lib/pleroma/web/federator/federator.ex View File

@@ -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 @@ defmodule Pleroma.Web.Federator 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 @@ defmodule Pleroma.Web.Federator 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 @@ defmodule Pleroma.Web.Federator 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 @@ defmodule Pleroma.Web.Federator 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)



+ 26
- 0
lib/pleroma/web/federator/publisher.ex View File

@@ -80,4 +80,30 @@ defmodule Pleroma.Web.Federator.Publisher 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

+ 7
- 0
lib/pleroma/web/mastodon_api/websocket_handler.ex View File

@@ -35,6 +35,13 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler 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} ->


+ 0
- 313
lib/pleroma/web/ostatus/activity_representer.ex View File

@@ -1,313 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 66
lib/pleroma/web/ostatus/feed_representer.ex View File

@@ -1,66 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 18
lib/pleroma/web/ostatus/handlers/delete_handler.ex View File

@@ -1,18 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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, local: false) do
delete
end
end
end

+ 0
- 26
lib/pleroma/web/ostatus/handlers/follow_handler.ex View File

@@ -1,26 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 168
lib/pleroma/web/ostatus/handlers/note_handler.ex View File

@@ -1,168 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 22
lib/pleroma/web/ostatus/handlers/unfollow_handler.ex View File

@@ -1,22 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 395
lib/pleroma/web/ostatus/ostatus.ex View File

@@ -1,395 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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/<link href='(.*)' rel='alternate' type='application\/atom\+xml'>/
@gs_regex ~r/<link title=.* href="(.*)" type="application\/atom\+xml" rel="alternate">/
@gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
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

+ 1
- 45
lib/pleroma/web/ostatus/ostatus_controller.ex View File

@@ -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 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|> 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


+ 0
- 41
lib/pleroma/web/ostatus/user_representer.ex View File

@@ -1,41 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 11
- 6
lib/pleroma/web/router.ex View File

@@ -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)

@@ -499,11 +509,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



+ 0
- 254
lib/pleroma/web/salmon/salmon.ex View File

@@ -1,254 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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 = """
<?xml version="1.0" encoding="UTF-8"?>
<me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
<me:data type="application/atom+xml">#{doc_base64}</me:data>
<me:encoding>#{encoding}</me:encoding>
<me:alg>#{alg}</me:alg>
<me:sig>#{signature}</me:sig>
</me:env>
"""

{: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

+ 3
- 3
lib/pleroma/web/streamer/streamer.ex View File

@@ -49,7 +49,7 @@ defmodule Pleroma.Web.Streamer do
end
end

defp handle_should_send(_) do
true
end
defp handle_should_send(:benchmark), do: false
defp handle_should_send(_), do: true
end

+ 0
- 2
lib/pleroma/web/templates/feed/feed/feed.xml.eex View File

@@ -10,8 +10,6 @@
<title><%= @user.nickname <> "'s timeline" %></title>
<updated><%= most_recent_update(@activities, @user) %></updated>
<logo><%= logo(@user) %></logo>
<link rel="hub" href="<%= websub_url(@conn, :websub_subscription_request, @user.nickname) %>"/>
<link rel="salmon" href="<%= o_status_url(@conn, :salmon_incoming, @user.nickname) %>"/>
<link rel="self" href="<%= '#{feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/>

<%= render @view_module, "_author.xml", assigns %>


+ 0
- 12
lib/pleroma/web/web_finger/web_finger.ex View File

@@ -108,7 +108,6 @@ defmodule Pleroma.Web.WebFinger 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 @@ defmodule Pleroma.Web.WebFinger do
"magic_key" => magic_key,
"topic" => topic,
"subject" => subject,
"salmon" => salmon,
"subscribe_address" => subscribe_address,
"ap_id" => ap_id
}
@@ -148,16 +146,6 @@ defmodule Pleroma.Web.WebFinger 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"])



+ 0
- 332
lib/pleroma/web/websub/websub.ex View File

@@ -1,332 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 20
lib/pleroma/web/websub/websub_client_subscription.ex View File

@@ -1,20 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 99
lib/pleroma/web/websub/websub_controller.ex View File

@@ -1,99 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 17
lib/pleroma/web/websub/websub_server_subscription.ex View File

@@ -1,17 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 4
lib/pleroma/workers/receiver_worker.ex View File

@@ -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


+ 0
- 26
lib/pleroma/workers/subscriber_worker.ex View File

@@ -1,26 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.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

+ 1
- 0
mix.exs View File

@@ -69,6 +69,7 @@ defmodule Pleroma.Mixfile do
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"]



+ 22
- 0
priv/repo/migrations/20190711042021_create_safe_jsonb_set.exs View File

@@ -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

+ 1
- 1
priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs View File

@@ -4,7 +4,7 @@ defmodule Pleroma.Repo.Migrations.CopyMutedToMutedNotifications do

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

+ 2
- 2
rel/files/bin/pleroma_ctl View File

@@ -141,8 +141,8 @@ else

ACTION="$1"
shift
if [ "$(echo \"$1\" | grep \"^-\" >/dev/null)" = false ]; then
echo "$1" | grep "^-" >/dev/null
if [ $? -eq 1 ]; then
SUBACTION="$1"
shift
fi


+ 34
- 1
test/conversation/participation_test.exs View File

@@ -23,6 +23,39 @@ defmodule Pleroma.Conversation.ParticipationTest 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).info.unread_conversation_count == 0
assert User.get_cached_by_id(other_user.id).info.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).info.unread_conversation_count == 1
assert User.get_cached_by_id(other_user.id).info.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 @@ defmodule Pleroma.Conversation.ParticipationTest 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)



+ 1
- 0
test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json View File

@@ -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":"@<a href=\"https://shitposter.club/users/9655\" class=\"h-card mention\" title=\"Solidarity for Pigs\">neimzr4luzerz</a> @<a href=\"https://gs.smuglo.li/user/2326\" class=\"h-card mention\" title=\"Dolus_McHonest\">dolus</a> 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"}

+ 1
- 0
test/fixtures/tesla_mock/moonman@shitposter.club.json View File

@@ -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<br>XMPP: moon@talk.shitposter.club<br>PRONOUNS: none of your business<br><br>Purported leftist kike piece of shit","tag":[],"type":"Person","url":"https://shitposter.club/users/moonman"}

+ 4
- 4
test/moderation_log_test.exs View File

@@ -24,13 +24,13 @@ defmodule Pleroma.ModerationLogTest do
{: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 @@ defmodule Pleroma.ModerationLogTest do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
subject: subject1,
subject: [subject1],
action: "grant",
permission: "moderator"
})
@@ -142,7 +142,7 @@ defmodule Pleroma.ModerationLogTest do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
subject: subject1,
subject: [subject1],
action: "revoke",
permission: "moderator"
})


+ 1
- 1
test/object/containment_test.exs View File

@@ -65,7 +65,7 @@ defmodule Pleroma.Object.ContainmentTest 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



+ 3
- 36
test/object/fetcher_test.exs View File

@@ -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 @@ defmodule Pleroma.Object.FetcherTest 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


+ 12
- 0
test/safe_jsonb_set_test.exs View File

@@ -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

+ 1
- 2
test/signature_test.exs View File

@@ -69,8 +69,7 @@ defmodule Pleroma.SignatureTest 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


+ 0
- 20
test/support/factory.ex View File

@@ -281,26 +281,6 @@ defmodule Pleroma.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",


+ 9
- 1
test/support/http_request_mock.ex View File

@@ -38,6 +38,14 @@ defmodule HttpRequestMock 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{
@@ -620,7 +628,7 @@ defmodule HttpRequestMock 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



+ 0
- 26
test/user_search_test.exs View File

@@ -65,21 +65,6 @@ defmodule Pleroma.UserSearchTest 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 +148,6 @@ defmodule Pleroma.UserSearchTest do
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"})



+ 0
- 22
test/user_test.exs View File

@@ -190,23 +190,6 @@ defmodule Pleroma.UserTest do
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])
@@ -474,11 +457,6 @@ defmodule Pleroma.UserTest 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"


+ 21
- 0
test/web/activity_pub/activity_pub_test.exs View File

@@ -41,6 +41,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest 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


+ 4
- 4
test/web/activity_pub/relay_test.exs View File

@@ -22,8 +22,8 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
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 +41,8 @@ defmodule Pleroma.Web.ActivityPub.RelayTest 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


+ 0
- 44
test/web/activity_pub/transmogrifier_test.exs View File

@@ -7,14 +7,11 @@ 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.CommonAPI
alias Pleroma.Web.OStatus
alias Pleroma.Web.Websub.WebsubClientSubscription

import Mock
import Pleroma.Factory
@@ -1181,32 +1178,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
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)

@@ -1371,21 +1342,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
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)
end
end

describe "actor rewriting" do
test "it fixes the actor URL property to be a proper URI" do
data = %{


+ 178
- 49
test/web/admin_api/admin_api_controller_test.exs View File

@@ -17,8 +17,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
alias Pleroma.Web.MediaProxy
import Pleroma.Factory

describe "/api/pleroma/admin/users" do
test "Delete" do
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, info: %{is_admin: true})
user = insert(:user)

@@ -30,15 +36,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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, info: %{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})

@@ -404,82 +431,72 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
"@#{admin.nickname} made @#{user.nickname} admin"
end

test "/:right DELETE, can remove from a permission group" do
test "/:right POST, can add to a permission group (multiple)" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user, info: %{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/#{user.nickname}/permission_group/admin")
|> post("/api/pleroma/admin/users/permission_group/admin", %{
nicknames: [user_one.nickname, user_two.nickname]
})

assert json_response(conn, 200) == %{
"is_admin" => false
"is_admin" => true
}

log_entry = Repo.one(ModerationLog)

assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} revoked admin role from @#{user.nickname}"
"@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
end
end

describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do
setup %{conn: conn} do
test "/:right DELETE, can remove from a permission group" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user, info: %{is_admin: true})

conn =
conn
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")

%{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}"
"@#{admin.nickname} revoked admin role from @#{user.nickname}"
end

test "activates the user", %{conn: conn, admin: admin} do
user = insert(:user, info: %{deactivated: true})
test "/:right DELETE, can remove from a permission group (multiple)" do
admin = insert(:user, info: %{is_admin: true})
user_one = insert(:user, info: %{is_admin: true})
user_two = insert(:user, info: %{is_admin: true})

conn =
conn
|> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true})
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]
})

user = User.get_cached_by_id(user.id)
assert user.info.deactivated == false
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} 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

@@ -1029,6 +1046,50 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
end

test "PATCH /api/pleroma/admin/users/activate" do
admin = insert(:user, info: %{is_admin: true})
user_one = insert(:user, info: %{deactivated: true})
user_two = insert(:user, info: %{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, info: %{is_admin: true})
user_one = insert(:user, info: %{deactivated: false})
user_two = insert(:user, info: %{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})
user = insert(:user)
@@ -1053,7 +1114,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest 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
@@ -2486,6 +2547,74 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
assert User.get_by_id(user.id).info.password_reset_pending == true
end
end

describe "relays" do
setup %{conn: conn} do
admin = insert(:user, info: %{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
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()

conn =
build_conn()
|> assign(:user, admin)
|> get("/api/pleroma/admin/relay")

assert json_response(conn, 200)["relays"] -- ["test-app.com", "test-app-42.com"] == []
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

# Needed for testing


+ 0
- 87
test/web/federator_test.exs View File

@@ -111,93 +111,6 @@ defmodule Pleroma.Web.FederatorTest 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


+ 10
- 4
test/web/mastodon_api/controllers/conversation_controller_test.exs View File

@@ -54,9 +54,9 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest 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).info.unread_conversation_count == 0
end

test "updates the last_status on reply", %{conn: conn} do
@@ -95,19 +95,23 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
"visibility" => "direct"
})

assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 0
assert User.get_cached_by_id(user_two.id).info.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_two.id).info.unread_conversation_count == 0

# The conversation is marked as unread on reply
{:ok, _} =
@@ -124,6 +128,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
|> json_response(200)

assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0

# A reply doesn't increment the user's unread_conversation_count if the conversation is unread
{:ok, _} =
@@ -134,6 +139,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
})

assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0
end

test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do


+ 3
- 3
test/web/mastodon_api/controllers/search_controller_test.exs View File

@@ -204,17 +204,17 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest 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"]


+ 1
- 6
test/web/mastodon_api/controllers/timeline_controller_test.exs View File

@@ -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])

@@ -75,8 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest 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"})

@@ -271,9 +269,6 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest 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)


+ 2
- 2
test/web/mastodon_api/views/account_view_test.exs View File

@@ -424,8 +424,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest 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"
})



+ 4
- 7
test/web/mastodon_api/views/status_view_test.exs View File

@@ -14,7 +14,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

@@ -230,17 +229,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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


+ 0
- 300
test/web/ostatus/activity_representer_test.exs View File

@@ -1,300 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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{<link type="text/html" href="https://mastodon.social/users/lambadalambda/updates/2314748" rel="alternate"/>}
)
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 = """
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<id>#{object_data["id"]}</id>
<title>New note by #{user.nickname}</title>
<content type="html">#{object_data["content"]}</content>
<published>#{object_data["published"]}</published>
<updated>#{object_data["published"]}</updated>
<ostatus:conversation ref="#{note_activity.data["context"]}">#{note_activity.data["context"]}</ostatus:conversation>
<link ref="#{note_activity.data["context"]}" rel="ostatus:conversation" />
<summary>#{object_data["summary"]}</summary>
<link type="application/atom+xml" href="#{object_data["id"]}" rel="self" />
<link type="text/html" href="#{object_data["id"]}" rel="alternate" />
<category term="2hu"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link name="2hu" rel="emoji" href="corndog.png" />
"""

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 = """
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<id>#{object.data["id"]}</id>
<title>New note by #{user.nickname}</title>
<content type="html">#{object.data["content"]}</content>
<published>#{object.data["published"]}</published>
<updated>#{object.data["published"]}</updated>
<ostatus:conversation ref="#{answer.data["context"]}">#{answer.data["context"]}</ostatus:conversation>
<link ref="#{answer.data["context"]}" rel="ostatus:conversation" />
<summary>2hu</summary>
<link type="application/atom+xml" href="#{object.data["id"]}" rel="self" />
<link type="text/html" href="#{object.data["id"]}" rel="alternate" />
<category term="2hu"/>
<thr:in-reply-to ref="#{note_object.data["id"]}" href="someurl" />
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<link name="2hu" rel="emoji" href="corndog.png" />
"""

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 = """
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
<id>#{announce.data["id"]}</id>
<title>#{user.nickname} repeated a notice</title>
<content type="html">RT #{object.data["content"]}</content>
<published>#{announce.data["published"]}</published>
<updated>#{announce.data["published"]}</updated>
<ostatus:conversation ref="#{announce.data["context"]}">#{announce.data["context"]}</ostatus:conversation>
<link ref="#{announce.data["context"]}" rel="ostatus:conversation" />
<link rel="self" type="application/atom+xml" href="#{announce.data["id"]}"/>
<activity:object>
#{note_xml}
</activity:object>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
note.data["actor"]
}"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
"""

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 = """
<activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
<id>#{like.data["id"]}</id>
<title>New favorite by #{user.nickname}</title>
<content type="html">#{user.nickname} favorited something</content>
<published>#{like.data["published"]}</published>
<updated>#{like.data["published"]}</updated>
<activity:object>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<id>#{note.data["id"]}</id>
</activity:object>
<ostatus:conversation ref="#{like.data["context"]}">#{like.data["context"]}</ostatus:conversation>
<link ref="#{like.data["context"]}" rel="ostatus:conversation" />
<link rel="self" type="application/atom+xml" href="#{like.data["id"]}"/>
<thr:in-reply-to ref="#{note.data["id"]}" />
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
note.data["actor"]
}"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
"""

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 = """
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/follow</activity:verb>
<id>#{activity.data["id"]}</id>
<title>#{follower.nickname} started following #{activity.data["object"]}</title>
<content type="html"> #{follower.nickname} started following #{activity.data["object"]}</content>
<published>#{activity.data["published"]}</published>
<updated>#{activity.data["published"]}</updated>
<activity:object>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<id>#{activity.data["object"]}</id>
<uri>#{activity.data["object"]}</uri>
</activity:object>
<link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
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 = """
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/unfollow</activity:verb>
<id>#{activity.data["id"]}</id>
<title>#{follower.nickname} stopped following #{followed.ap_id}</title>
<content type="html"> #{follower.nickname} stopped following #{followed.ap_id}</content>
<published>#{activity.data["published"]}</published>
<updated>#{activity.data["published"]}</updated>
<activity:object>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<id>#{followed.ap_id}</id>
<uri>#{followed.ap_id}</uri>
</activity:object>
<link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
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 = """
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/delete</activity:verb>
<id>#{activity.data["object"]}</id>
<title>An object was deleted</title>
<content type="html">An object was deleted</content>
<published>#{activity.data["published"]}</published>
<updated>#{activity.data["published"]}</updated>
"""

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

+ 0
- 59
test/web/ostatus/feed_representer_test.exs View File

@@ -1,59 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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 = """
<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>#{OStatus.feed_path(user)}</id>
<title>#{user.nickname}'s timeline</title>
<updated>#{most_recent_update}</updated>
<logo>#{User.avatar_url(user)}</logo>
<link rel="hub" href="#{OStatus.pubsub_path(user)}" />
<link rel="salmon" href="#{OStatus.salmon_path(user)}" />
<link rel="self" href="#{OStatus.feed_path(user)}" type="application/atom+xml" />
<author>
#{user_xml}
</author>
<link rel="next" href="#{OStatus.feed_path(user)}?max_id=#{note_activity.id}" type="application/atom+xml" />
<entry>
#{entry_xml}
</entry>
</feed>
"""

assert clean(res) == clean(expected)
end

defp clean(string) do
String.replace(string, ~r/\s/, "")
end
end

+ 0
- 48
test/web/ostatus/incoming_documents/delete_handling_test.exs View File

@@ -1,48 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 100
test/web/ostatus/ostatus_controller_test.exs View File

@@ -11,7 +11,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
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 +21,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 +71,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest 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 +98,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest 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"]))


+ 0
- 645
test/web/ostatus/ostatus_test.exs View File

@@ -1,645 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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"] ==
"@<a href=\"https://gs.archae.me/user/4687\" class=\"h-card u-url p-nickname mention\" title=\"shpbot\">shpbot</a> 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

+ 0
- 38
test/web/ostatus/user_representer_test.exs View File

@@ -1,38 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# 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 = """
<id>#{user.ap_id}</id>
<activity:object>http://activitystrea.ms/schema/1.0/person</activity:object>
<uri>#{user.ap_id}</uri>
<poco:preferredUsername>#{user.nickname}</poco:preferredUsername>
<poco:displayName>#{user.name}</poco:displayName>
<poco:note>#{user.bio}</poco:note>
<summary>#{user.bio}</summary>
<name>#{user.nickname}</name>
<link rel="avatar" href="#{User.avatar_url(user)}" />
<link rel="header" href="#{User.banner_url(user)}" />
<ap_enabled>true</ap_enabled>
"""

assert clean(res) == clean(expected)
end

defp clean(string) do
String.replace(string, ~r/\s/, "")
end
end

+ 0
- 101
test/web/salmon/salmon_test.exs View File

@@ -1,101 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 27
test/web/web_finger/web_finger_test.exs View File

@@ -45,19 +45,6 @@ defmodule Pleroma.Web.WebFingerTest 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 @@ defmodule Pleroma.Web.WebFingerTest do
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"



+ 0
- 86
test/web/websub/websub_controller_test.exs View File

@@ -1,86 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

+ 0
- 236
test/web/websub/websub_test.exs View File

@@ -1,236 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.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

Loading…
Cancel
Save