forked from AkkomaGang/akkoma
Merge branch 'develop' into feature/push-subject-for-dm
This commit is contained in:
commit
dcb23a85b6
88 changed files with 1698 additions and 4224 deletions
|
@ -15,6 +15,7 @@ cache:
|
||||||
stages:
|
stages:
|
||||||
- build
|
- build
|
||||||
- test
|
- test
|
||||||
|
- benchmark
|
||||||
- deploy
|
- deploy
|
||||||
- release
|
- release
|
||||||
|
|
||||||
|
@ -28,6 +29,36 @@ build:
|
||||||
- mix deps.get
|
- mix deps.get
|
||||||
- mix compile --force
|
- 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:
|
unit-testing:
|
||||||
stage: test
|
stage: test
|
||||||
services:
|
services:
|
||||||
|
|
100
CHANGELOG.md
100
CHANGELOG.md
|
@ -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/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### Added
|
||||||
- Refreshing poll results for remote polls
|
- 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
|
- Job queue stats to the healthcheck page
|
||||||
- Admin API: Add ability to require password reset
|
- Admin API: Add ability to require password reset
|
||||||
- Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition)
|
- 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 `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
|
- 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/)
|
- 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
|
- 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
|
- 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
|
### Changed
|
||||||
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
|
- **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
|
- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities
|
||||||
- OStatus: Extract RSS functionality
|
- OStatus: Extract RSS functionality
|
||||||
- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`)
|
- 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
|
### 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: 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`
|
- 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
|
</details>
|
||||||
- Report emails now include functional links to profiles of remote user accounts
|
|
||||||
|
|
||||||
## [1.1.0] - 2019-??-??
|
## [1.1.2] - 2019-10-18
|
||||||
**Breaking:** The stable branch has been changed from `master` to `stable`, `master` now points to `release/1.0`
|
### Fixed
|
||||||
|
- `pleroma_ctl` trying to connect to a running instance when generating the config, which of course doesn't exist.
|
||||||
|
|
||||||
|
## [1.1.1] - 2019-10-18
|
||||||
|
### Fixed
|
||||||
|
- One of the migrations between 1.0.0 and 1.1.0 wiping user info of the relay user because of unexpected behavior of postgresql's `jsonb_set`, resulting in inability to post in the default configuration. If you were affected, please run the following query in postgres console, the relay user will be recreated automatically:
|
||||||
|
```
|
||||||
|
delete from users where ap_id = 'https://your.instance.hostname/relay';
|
||||||
|
```
|
||||||
|
- Bad user search matches
|
||||||
|
|
||||||
|
## [1.1.0] - 2019-10-14
|
||||||
|
**Breaking:** The stable branch has been changed from `master` to `stable`. If you want to keep using 1.0, the `release/1.0` branch will receive security updates for 6 months after 1.1 release.
|
||||||
|
|
||||||
|
**OTP Note:** `pleroma_ctl` in 1.0 defaults to `master` and doesn't support specifying arbitrary branches, making `./pleroma_ctl update` fail. To fix this, fetch a version of `pleroma_ctl` from 1.1 using the command below and proceed with the update normally:
|
||||||
|
```
|
||||||
|
curl -Lo ./bin/pleroma_ctl 'https://git.pleroma.social/pleroma/pleroma/raw/develop/rel/files/bin/pleroma_ctl'
|
||||||
|
```
|
||||||
### Security
|
### Security
|
||||||
- Mastodon API: respect post privacy in `/api/v1/statuses/:id/{favourited,reblogged}_by`
|
- 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
|
- **Breaking:** GNU Social API with Qvitter extensions support
|
||||||
- Emoji: Remove longfox emojis.
|
- Emoji: Remove longfox emojis.
|
||||||
- Remove `Reply-To` header from report emails for admins.
|
- Remove `Reply-To` header from report emails for admins.
|
||||||
|
- ActivityPub: The `/objects/:uuid/likes` endpoint.
|
||||||
|
|
||||||
### Changed
|
### 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: 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:** 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/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: added `config/description.exs`, from which `docs/config.md` is generated
|
||||||
- Configuration: OpenGraph and TwitterCard providers enabled by default
|
- Configuration: OpenGraph and TwitterCard providers enabled by default
|
||||||
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
|
- 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
|
- 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 `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
|
||||||
- NodeInfo: Return `mailerEnabled` in `metadata`
|
- 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)
|
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
|
||||||
- Improve digest email template
|
- Improve digest email template
|
||||||
– Pagination: (optional) return `total` alongside with `items` when paginating
|
– 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
|
### Fixed
|
||||||
- Following from Osada
|
- 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: 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: 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: 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: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`)
|
||||||
- Mastodon API, streaming: Fix filtering of notifications based on blocks/mutes/thread mutes
|
- Mastodon API, 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
|
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
|
||||||
- Existing user id not being preserved on insert conflict
|
- 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: 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.
|
- Rich Media: The crawled URL is now spliced into the rich media data.
|
||||||
- ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification.
|
- ActivityPub 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.
|
- ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set
|
||||||
- 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: Deactivated user deletion
|
- ActivityPub: Deactivated user deletion
|
||||||
- ActivityPub: Fix `/users/:nickname/inbox` crashing without an authenticated user
|
- ActivityPub: Fix `/users/:nickname/inbox` crashing without an authenticated user
|
||||||
- MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled
|
- 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
|
- ActivityPub: Correct addressing of Undo.
|
||||||
- Mastodon API: `exclude_replies` is correctly handled again.
|
- 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
|
### Added
|
||||||
- Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically.
|
- 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: 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, 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, 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 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 muting/unmuting notifications
|
||||||
- Mastodon API: Add support for the `blocked_by` attribute in the relationship API (`GET /api/v1/accounts/relationships`). <https://github.com/tootsuite/mastodon/pull/10373>
|
- Mastodon API: Add support for the `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: 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: /api/v1/accounts/:id/statuses now supports nicknames or user id
|
||||||
- Mastodon API: Improve support for the user profile custom fields
|
- 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 users' tags when querying reports
|
||||||
- Admin API: Return avatar and display name when querying users
|
- Admin API: Return avatar and display name when querying users
|
||||||
- Admin API: Allow querying user by ID
|
- 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: Add `/api/v1/pleroma/accounts/confirmation_resend?email=<email>` for resending account confirmation.
|
||||||
- Pleroma API: Email change endpoint.
|
- Pleroma API: Email change endpoint.
|
||||||
- Admin API: Added moderation log
|
- 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)
|
- 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
|
- Reverse Proxy: Do not retry failed requests to limit pressure on the peer
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
229
benchmarks/load_testing/fetcher.ex
Normal file
229
benchmarks/load_testing/fetcher.ex
Normal 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
benchmarks/load_testing/generator.ex
Normal file
352
benchmarks/load_testing/generator.ex
Normal 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
benchmarks/load_testing/helper.ex
Normal file
11
benchmarks/load_testing/helper.ex
Normal 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
benchmarks/mix/tasks/pleroma/load_testing.ex
Normal file
134
benchmarks/mix/tasks/pleroma/load_testing.ex
Normal 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
config/benchmark.exs
Normal file
84
config/benchmark.exs
Normal 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
|
|
@ -59,10 +59,6 @@
|
||||||
_ -> []
|
_ -> []
|
||||||
end
|
end
|
||||||
|
|
||||||
scheduled_jobs =
|
|
||||||
scheduled_jobs ++
|
|
||||||
[{"0 */6 * * * *", {Pleroma.Web.Websub, :refresh_subscriptions, []}}]
|
|
||||||
|
|
||||||
config :pleroma, Pleroma.Scheduler,
|
config :pleroma, Pleroma.Scheduler,
|
||||||
global: true,
|
global: true,
|
||||||
overlap: true,
|
overlap: true,
|
||||||
|
@ -243,9 +239,7 @@
|
||||||
federation_incoming_replies_max_depth: 100,
|
federation_incoming_replies_max_depth: 100,
|
||||||
federation_reachability_timeout_days: 7,
|
federation_reachability_timeout_days: 7,
|
||||||
federation_publisher_modules: [
|
federation_publisher_modules: [
|
||||||
Pleroma.Web.ActivityPub.Publisher,
|
Pleroma.Web.ActivityPub.Publisher
|
||||||
Pleroma.Web.Websub,
|
|
||||||
Pleroma.Web.Salmon
|
|
||||||
],
|
],
|
||||||
allow_relay: true,
|
allow_relay: true,
|
||||||
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
|
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
|
||||||
|
|
|
@ -581,9 +581,7 @@
|
||||||
type: [:list, :module],
|
type: [:list, :module],
|
||||||
description: "List of modules for federation publishing",
|
description: "List of modules for federation publishing",
|
||||||
suggestions: [
|
suggestions: [
|
||||||
Pleroma.Web.ActivityPub.Publisher,
|
Pleroma.Web.ActivityPub.Publisher
|
||||||
Pleroma.Web.Websub,
|
|
||||||
Pleroma.Web.Salmo
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Config
|
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 :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
|
||||||
|
|
||||||
config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"
|
config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"
|
||||||
|
|
|
@ -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
|
### Remove a user
|
||||||
|
|
||||||
|
@ -56,6 +56,15 @@ Authentication is required and the user must be an admin.
|
||||||
- `nickname`
|
- `nickname`
|
||||||
- Response: User’s 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
|
### Create a user
|
||||||
|
|
||||||
- Method: `POST`
|
- 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
|
- Params: none
|
||||||
- Response:
|
- Response:
|
||||||
- On failure: `{"error": "…"}`
|
- On failure: `{"error": "…"}`
|
||||||
- On success: JSON of the `user.info`
|
- 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
|
### Remove user from permission group
|
||||||
|
|
||||||
- Method: `DELETE`
|
|
||||||
- Params: none
|
- Params: none
|
||||||
- Response:
|
- Response:
|
||||||
- On failure: `{"error": "…"}`
|
- On failure: `{"error": "…"}`
|
||||||
- On success: JSON of the `user.info`
|
- On success: JSON of the `user.info`
|
||||||
- Note: An admin cannot revoke their own admin status.
|
- 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
|
### Active or deactivate a user
|
||||||
|
|
||||||
- Method: `PUT`
|
|
||||||
- Params:
|
- Params:
|
||||||
- `nickname`
|
- `nickname`
|
||||||
- `status` BOOLEAN field, false value means deactivation.
|
- `status` BOOLEAN field, false value means deactivation.
|
||||||
|
@ -222,6 +289,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||||
- Response:
|
- Response:
|
||||||
- On success: URL of the unfollowed relay
|
- 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`
|
## `/api/pleroma/admin/users/invite_token`
|
||||||
|
|
||||||
### Create an account registration invite token
|
### Create an account registration invite token
|
||||||
|
|
|
@ -28,7 +28,7 @@ def run(["remove_embedded_objects" | args]) do
|
||||||
Logger.info("Removing embedded objects")
|
Logger.info("Removing embedded objects")
|
||||||
|
|
||||||
Repo.query!(
|
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
|
timeout: :infinity
|
||||||
)
|
)
|
||||||
|
@ -126,7 +126,7 @@ def run(["fix_likes_collections"]) do
|
||||||
set: [
|
set: [
|
||||||
data:
|
data:
|
||||||
fragment(
|
fragment(
|
||||||
"jsonb_set(?, '{likes}', '[]'::jsonb, true)",
|
"safe_jsonb_set(?, '{likes}', '[]'::jsonb, true)",
|
||||||
object.data
|
object.data
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
defmodule Mix.Tasks.Pleroma.Relay do
|
defmodule Mix.Tasks.Pleroma.Relay do
|
||||||
use Mix.Task
|
use Mix.Task
|
||||||
import Mix.Pleroma
|
import Mix.Pleroma
|
||||||
alias Pleroma.User
|
|
||||||
alias Pleroma.Web.ActivityPub.Relay
|
alias Pleroma.Web.ActivityPub.Relay
|
||||||
|
|
||||||
@shortdoc "Manages remote relays"
|
@shortdoc "Manages remote relays"
|
||||||
|
@ -36,13 +35,10 @@ def run(["unfollow", target]) do
|
||||||
def run(["list"]) do
|
def run(["list"]) do
|
||||||
start_pleroma()
|
start_pleroma()
|
||||||
|
|
||||||
with %User{following: following} = _user <- Relay.get_actor() do
|
with {:ok, list} <- Relay.list() do
|
||||||
following
|
list |> Enum.each(&shell_info(&1))
|
||||||
|> Enum.map(fn entry -> URI.parse(entry).host end)
|
|
||||||
|> Enum.uniq()
|
|
||||||
|> Enum.each(&shell_info(&1))
|
|
||||||
else
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -161,11 +161,6 @@ defp task_children(:test) do
|
||||||
id: :web_push_init,
|
id: :web_push_init,
|
||||||
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
|
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
|
||||||
restart: :temporary
|
restart: :temporary
|
||||||
},
|
|
||||||
%{
|
|
||||||
id: :federator_init,
|
|
||||||
start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
|
|
||||||
restart: :temporary
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
@ -177,11 +172,6 @@ defp task_children(_) do
|
||||||
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
|
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
|
||||||
restart: :temporary
|
restart: :temporary
|
||||||
},
|
},
|
||||||
%{
|
|
||||||
id: :federator_init,
|
|
||||||
start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
|
|
||||||
restart: :temporary
|
|
||||||
},
|
|
||||||
%{
|
%{
|
||||||
id: :internal_fetch_init,
|
id: :internal_fetch_init,
|
||||||
start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
|
start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
|
||||||
|
|
|
@ -48,6 +48,12 @@ def read_cng(struct, params) do
|
||||||
|> validate_required([:read])
|
|> validate_required([:read])
|
||||||
end
|
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
|
def mark_as_read(participation) do
|
||||||
participation
|
participation
|
||||||
|> read_cng(%{read: true})
|
|> read_cng(%{read: true})
|
||||||
|
|
|
@ -86,18 +86,18 @@ defp parse_datetime(datetime) do
|
||||||
parsed_datetime
|
parsed_datetime
|
||||||
end
|
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}
|
{:ok, ModerationLog} | {:error, any}
|
||||||
def insert_log(%{
|
def insert_log(%{
|
||||||
actor: %User{} = actor,
|
actor: %User{} = actor,
|
||||||
subject: %User{} = subject,
|
subject: subjects,
|
||||||
action: action,
|
action: action,
|
||||||
permission: permission
|
permission: permission
|
||||||
}) do
|
}) do
|
||||||
%ModerationLog{
|
%ModerationLog{
|
||||||
data: %{
|
data: %{
|
||||||
"actor" => user_to_map(actor),
|
"actor" => user_to_map(actor),
|
||||||
"subject" => user_to_map(subject),
|
"subject" => user_to_map(subjects),
|
||||||
"action" => action,
|
"action" => action,
|
||||||
"permission" => permission,
|
"permission" => permission,
|
||||||
"message" => ""
|
"message" => ""
|
||||||
|
@ -303,13 +303,16 @@ def insert_log(%{
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
|
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
|
||||||
|
|
||||||
defp insert_log_entry_with_message(entry) do
|
defp insert_log_entry_with_message(entry) do
|
||||||
entry.data["message"]
|
entry.data["message"]
|
||||||
|> put_in(get_log_entry_message(entry))
|
|> put_in(get_log_entry_message(entry))
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
end
|
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
|
defp user_to_map(%User{} = user) do
|
||||||
user
|
user
|
||||||
|> Map.from_struct()
|
|> Map.from_struct()
|
||||||
|
@ -349,10 +352,10 @@ def get_log_entry_message(%ModerationLog{
|
||||||
data: %{
|
data: %{
|
||||||
"actor" => %{"nickname" => actor_nickname},
|
"actor" => %{"nickname" => actor_nickname},
|
||||||
"action" => "delete",
|
"action" => "delete",
|
||||||
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
|
"subject" => subjects
|
||||||
}
|
}
|
||||||
}) do
|
}) do
|
||||||
"@#{actor_nickname} deleted user @#{subject_nickname}"
|
"@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||||
|
@ -363,12 +366,7 @@ def get_log_entry_message(%ModerationLog{
|
||||||
"subjects" => subjects
|
"subjects" => subjects
|
||||||
}
|
}
|
||||||
}) do
|
}) do
|
||||||
nicknames =
|
"@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
|
||||||
subjects
|
|
||||||
|> Enum.map(&"@#{&1["nickname"]}")
|
|
||||||
|> Enum.join(", ")
|
|
||||||
|
|
||||||
"@#{actor_nickname} created users: #{nicknames}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||||
|
@ -376,10 +374,10 @@ def get_log_entry_message(%ModerationLog{
|
||||||
data: %{
|
data: %{
|
||||||
"actor" => %{"nickname" => actor_nickname},
|
"actor" => %{"nickname" => actor_nickname},
|
||||||
"action" => "activate",
|
"action" => "activate",
|
||||||
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
|
"subject" => users
|
||||||
}
|
}
|
||||||
}) do
|
}) do
|
||||||
"@#{actor_nickname} activated user @#{subject_nickname}"
|
"@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||||
|
@ -387,10 +385,10 @@ def get_log_entry_message(%ModerationLog{
|
||||||
data: %{
|
data: %{
|
||||||
"actor" => %{"nickname" => actor_nickname},
|
"actor" => %{"nickname" => actor_nickname},
|
||||||
"action" => "deactivate",
|
"action" => "deactivate",
|
||||||
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
|
"subject" => users
|
||||||
}
|
}
|
||||||
}) do
|
}) do
|
||||||
"@#{actor_nickname} deactivated user @#{subject_nickname}"
|
"@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||||
|
@ -402,14 +400,9 @@ def get_log_entry_message(%ModerationLog{
|
||||||
"action" => "tag"
|
"action" => "tag"
|
||||||
}
|
}
|
||||||
}) do
|
}) do
|
||||||
nicknames_string =
|
|
||||||
nicknames
|
|
||||||
|> Enum.map(&"@#{&1}")
|
|
||||||
|> Enum.join(", ")
|
|
||||||
|
|
||||||
tags_string = tags |> 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
|
end
|
||||||
|
|
||||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||||
|
@ -421,14 +414,9 @@ def get_log_entry_message(%ModerationLog{
|
||||||
"action" => "untag"
|
"action" => "untag"
|
||||||
}
|
}
|
||||||
}) do
|
}) do
|
||||||
nicknames_string =
|
|
||||||
nicknames
|
|
||||||
|> Enum.map(&"@#{&1}")
|
|
||||||
|> Enum.join(", ")
|
|
||||||
|
|
||||||
tags_string = tags |> 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
|
end
|
||||||
|
|
||||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||||
|
@ -436,11 +424,11 @@ def get_log_entry_message(%ModerationLog{
|
||||||
data: %{
|
data: %{
|
||||||
"actor" => %{"nickname" => actor_nickname},
|
"actor" => %{"nickname" => actor_nickname},
|
||||||
"action" => "grant",
|
"action" => "grant",
|
||||||
"subject" => %{"nickname" => subject_nickname},
|
"subject" => users,
|
||||||
"permission" => permission
|
"permission" => permission
|
||||||
}
|
}
|
||||||
}) do
|
}) do
|
||||||
"@#{actor_nickname} made @#{subject_nickname} #{permission}"
|
"@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||||
|
@ -448,11 +436,11 @@ def get_log_entry_message(%ModerationLog{
|
||||||
data: %{
|
data: %{
|
||||||
"actor" => %{"nickname" => actor_nickname},
|
"actor" => %{"nickname" => actor_nickname},
|
||||||
"action" => "revoke",
|
"action" => "revoke",
|
||||||
"subject" => %{"nickname" => subject_nickname},
|
"subject" => users,
|
||||||
"permission" => permission
|
"permission" => permission
|
||||||
}
|
}
|
||||||
}) do
|
}) do
|
||||||
"@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}"
|
"@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||||
|
@ -551,4 +539,16 @@ def get_log_entry_message(%ModerationLog{
|
||||||
}) do
|
}) do
|
||||||
"@#{actor_nickname} deleted status ##{subject_id}"
|
"@#{actor_nickname} deleted status ##{subject_id}"
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -181,7 +181,7 @@ def increase_replies_count(ap_id) do
|
||||||
data:
|
data:
|
||||||
fragment(
|
fragment(
|
||||||
"""
|
"""
|
||||||
jsonb_set(?, '{repliesCount}',
|
safe_jsonb_set(?, '{repliesCount}',
|
||||||
(coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
|
(coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
|
||||||
""",
|
""",
|
||||||
o.data,
|
o.data,
|
||||||
|
@ -204,7 +204,7 @@ def decrease_replies_count(ap_id) do
|
||||||
data:
|
data:
|
||||||
fragment(
|
fragment(
|
||||||
"""
|
"""
|
||||||
jsonb_set(?, '{repliesCount}',
|
safe_jsonb_set(?, '{repliesCount}',
|
||||||
(greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
|
(greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
|
||||||
""",
|
""",
|
||||||
o.data,
|
o.data,
|
||||||
|
|
|
@ -32,6 +32,23 @@ def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor)
|
||||||
get_actor(%{"actor" => actor})
|
get_actor(%{"actor" => actor})
|
||||||
end
|
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 """
|
@doc """
|
||||||
Checks that an imported AP object's actor matches the domain it came from.
|
Checks that an imported AP object's actor matches the domain it came from.
|
||||||
"""
|
"""
|
||||||
|
@ -41,11 +58,7 @@ def contain_origin(id, %{"actor" => _actor} = params) do
|
||||||
id_uri = URI.parse(id)
|
id_uri = URI.parse(id)
|
||||||
actor_uri = URI.parse(get_actor(params))
|
actor_uri = URI.parse(get_actor(params))
|
||||||
|
|
||||||
if id_uri.host == actor_uri.host do
|
compare_uris(actor_uri, id_uri)
|
||||||
:ok
|
|
||||||
else
|
|
||||||
:error
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def contain_origin(id, %{"attributedTo" => actor} = params),
|
def contain_origin(id, %{"attributedTo" => actor} = params),
|
||||||
|
@ -57,11 +70,7 @@ def contain_origin_from_id(id, %{"id" => other_id} = _params) do
|
||||||
id_uri = URI.parse(id)
|
id_uri = URI.parse(id)
|
||||||
other_uri = URI.parse(other_id)
|
other_uri = URI.parse(other_id)
|
||||||
|
|
||||||
if id_uri.host == other_uri.host do
|
compare_uris(id_uri, other_uri)
|
||||||
:ok
|
|
||||||
else
|
|
||||||
:error
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
|
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
|
||||||
|
|
|
@ -10,7 +10,6 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
alias Pleroma.Signature
|
alias Pleroma.Signature
|
||||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
alias Pleroma.Web.OStatus
|
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
@ -67,7 +66,8 @@ def fetch_object_from_id(id, options \\ []) do
|
||||||
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
|
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
|
||||||
params <- prepare_activity_params(data),
|
params <- prepare_activity_params(data),
|
||||||
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
|
{: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{} = object} <-
|
||||||
{:object, data, Object.normalize(activity, false)} do
|
{:object, data, Object.normalize(activity, false)} do
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
|
@ -75,9 +75,12 @@ def fetch_object_from_id(id, options \\ []) do
|
||||||
{:containment, _} ->
|
{:containment, _} ->
|
||||||
{:error, "Object containment failed."}
|
{:error, "Object containment failed."}
|
||||||
|
|
||||||
{:error, {:reject, nil}} ->
|
{:transmogrifier, {:error, {:reject, nil}}} ->
|
||||||
{:reject, nil}
|
{:reject, nil}
|
||||||
|
|
||||||
|
{:transmogrifier, _} ->
|
||||||
|
{:error, "Transmogrifier failure."}
|
||||||
|
|
||||||
{:object, data, nil} ->
|
{:object, data, nil} ->
|
||||||
reinject_object(%Object{}, data)
|
reinject_object(%Object{}, data)
|
||||||
|
|
||||||
|
@ -87,15 +90,8 @@ def fetch_object_from_id(id, options \\ []) do
|
||||||
{:fetch_object, %Object{} = object} ->
|
{:fetch_object, %Object{} = object} ->
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
|
|
||||||
_e ->
|
e ->
|
||||||
# Only fallback when receiving a fetch/normalization error with ActivityPub
|
e
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -114,7 +110,8 @@ def fetch_object_from_id!(id, options \\ []) do
|
||||||
with {:ok, object} <- fetch_object_from_id(id, options) do
|
with {:ok, object} <- fetch_object_from_id(id, options) do
|
||||||
object
|
object
|
||||||
else
|
else
|
||||||
_e ->
|
e ->
|
||||||
|
Logger.error("Error while fetching #{id}: #{inspect(e)}")
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -161,7 +158,7 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
||||||
|
|
||||||
Logger.debug("Fetch headers: #{inspect(headers)}")
|
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, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers),
|
||||||
{:ok, data} <- Jason.decode(body),
|
{:ok, data} <- Jason.decode(body),
|
||||||
:ok <- Containment.contain_origin_from_id(id, data) do
|
:ok <- Containment.contain_origin_from_id(id, data) do
|
||||||
|
@ -170,6 +167,9 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
||||||
{:ok, %{status: code}} when code in [404, 410] ->
|
{:ok, %{status: code}} when code in [404, 410] ->
|
||||||
{:error, "Object has been deleted"}
|
{:error, "Object has been deleted"}
|
||||||
|
|
||||||
|
{:scheme, _} ->
|
||||||
|
{:error, "Unsupported URI scheme"}
|
||||||
|
|
||||||
e ->
|
e ->
|
||||||
{:error, e}
|
{:error, e}
|
||||||
end
|
end
|
||||||
|
|
|
@ -105,7 +105,7 @@ defp get_opts(opts) do
|
||||||
{Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
|
{Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
|
||||||
end
|
end
|
||||||
|
|
||||||
opts = %{
|
%{
|
||||||
activity_type: Keyword.get(opts, :activity_type, activity_type),
|
activity_type: Keyword.get(opts, :activity_type, activity_type),
|
||||||
size_limit: Keyword.get(opts, :size_limit, size_limit),
|
size_limit: Keyword.get(opts, :size_limit, size_limit),
|
||||||
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
|
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
|
||||||
|
@ -118,37 +118,6 @@ defp get_opts(opts) do
|
||||||
Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
|
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
|
end
|
||||||
|
|
||||||
defp prepare_upload(%Plug.Upload{} = file, opts) do
|
defp prepare_upload(%Plug.Upload{} = file, opts) do
|
||||||
|
|
|
@ -26,9 +26,7 @@ defmodule Pleroma.User do
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
|
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
|
||||||
alias Pleroma.Web.OAuth
|
alias Pleroma.Web.OAuth
|
||||||
alias Pleroma.Web.OStatus
|
|
||||||
alias Pleroma.Web.RelMe
|
alias Pleroma.Web.RelMe
|
||||||
alias Pleroma.Web.Websub
|
|
||||||
alias Pleroma.Workers.BackgroundWorker
|
alias Pleroma.Workers.BackgroundWorker
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
@ -437,10 +435,6 @@ def follow(%User{} = follower, %User{info: info} = followed) do
|
||||||
{:error, "Could not follow user: #{followed.nickname} blocked you."}
|
{:error, "Could not follow user: #{followed.nickname} blocked you."}
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
if !followed.local && follower.local && !ap_enabled?(followed) do
|
|
||||||
Websub.subscribe(follower, followed)
|
|
||||||
end
|
|
||||||
|
|
||||||
q =
|
q =
|
||||||
from(u in User,
|
from(u in User,
|
||||||
where: u.id == ^follower.id,
|
where: u.id == ^follower.id,
|
||||||
|
@ -614,12 +608,7 @@ def get_cached_user_info(user) do
|
||||||
Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
|
Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_by_nickname(nickname) do
|
def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
|
||||||
case ActivityPub.make_user_from_nickname(nickname) do
|
|
||||||
{:ok, user} -> {:ok, user}
|
|
||||||
_ -> OStatus.make_user(nickname)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_or_fetch_by_nickname(nickname) do
|
def get_or_fetch_by_nickname(nickname) do
|
||||||
with %User{} = user <- get_by_nickname(nickname) do
|
with %User{} = user <- get_by_nickname(nickname) do
|
||||||
|
@ -725,7 +714,7 @@ def increase_note_count(%User{} = user) do
|
||||||
set: [
|
set: [
|
||||||
info:
|
info:
|
||||||
fragment(
|
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,
|
||||||
u.info
|
u.info
|
||||||
)
|
)
|
||||||
|
@ -746,7 +735,7 @@ def decrease_note_count(%User{} = user) do
|
||||||
set: [
|
set: [
|
||||||
info:
|
info:
|
||||||
fragment(
|
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,
|
||||||
u.info
|
u.info
|
||||||
)
|
)
|
||||||
|
@ -816,7 +805,7 @@ def update_follower_count(%User{} = user) do
|
||||||
set: [
|
set: [
|
||||||
info:
|
info:
|
||||||
fragment(
|
fragment(
|
||||||
"jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
|
"safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
|
||||||
u.info,
|
u.info,
|
||||||
s.count
|
s.count
|
||||||
)
|
)
|
||||||
|
@ -1059,7 +1048,15 @@ def deactivate_async(user, status \\ true) do
|
||||||
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
|
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
|
||||||
end
|
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
|
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_followers(user), &invalidate_cache/1)
|
||||||
Enum.each(get_friends(user), &update_follower_count/1)
|
Enum.each(get_friends(user), &update_follower_count/1)
|
||||||
|
@ -1072,6 +1069,10 @@ def update_notification_settings(%User{} = user, settings \\ %{}) do
|
||||||
update_info(user, &User.Info.update_notification_settings(&1, settings))
|
update_info(user, &User.Info.update_notification_settings(&1, settings))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete(users) when is_list(users) do
|
||||||
|
for user <- users, do: delete(user)
|
||||||
|
end
|
||||||
|
|
||||||
def delete(%User{} = user) do
|
def delete(%User{} = user) do
|
||||||
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
|
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
|
||||||
end
|
end
|
||||||
|
@ -1234,18 +1235,7 @@ def html_filter_policy(%User{info: %{no_rich_text: true}}) do
|
||||||
|
|
||||||
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
|
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
|
||||||
|
|
||||||
def fetch_by_ap_id(ap_id) do
|
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
|
||||||
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 get_or_fetch_by_ap_id(ap_id) do
|
def get_or_fetch_by_ap_id(ap_id) do
|
||||||
user = get_cached_by_ap_id(ap_id)
|
user = get_cached_by_ap_id(ap_id)
|
||||||
|
@ -1300,11 +1290,6 @@ def public_key_from_info(%{
|
||||||
{:ok, key}
|
{:ok, key}
|
||||||
end
|
end
|
||||||
|
|
||||||
# OStatus Magic Key
|
|
||||||
def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
|
|
||||||
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def public_key_from_info(_), do: {:error, "not found key"}
|
def public_key_from_info(_), do: {:error, "not found key"}
|
||||||
|
|
||||||
def get_public_key_for_ap_id(ap_id) do
|
def get_public_key_for_ap_id(ap_id) do
|
||||||
|
@ -1625,6 +1610,12 @@ def change_info(user, fun) do
|
||||||
|
|
||||||
`fun` is called with the `user.info`.
|
`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
|
def update_info(user, fun) do
|
||||||
user
|
user
|
||||||
|> change_info(fun)
|
|> change_info(fun)
|
||||||
|
|
|
@ -39,9 +39,6 @@ defmodule Pleroma.User.Info do
|
||||||
field(:settings, :map, default: nil)
|
field(:settings, :map, default: nil)
|
||||||
field(:magic_key, :string, default: nil)
|
field(:magic_key, :string, default: nil)
|
||||||
field(:uri, :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_followers_count, :boolean, default: false)
|
||||||
field(:hide_follows_count, :boolean, default: false)
|
field(:hide_follows_count, :boolean, default: false)
|
||||||
field(:hide_followers, :boolean, default: false)
|
field(:hide_followers, :boolean, default: false)
|
||||||
|
@ -262,9 +259,6 @@ def remote_user_creation(info, params) do
|
||||||
:locked,
|
:locked,
|
||||||
:magic_key,
|
:magic_key,
|
||||||
:uri,
|
:uri,
|
||||||
:hub,
|
|
||||||
:topic,
|
|
||||||
:salmon,
|
|
||||||
:hide_followers,
|
:hide_followers,
|
||||||
:hide_follows,
|
:hide_follows,
|
||||||
:hide_followers_count,
|
:hide_followers_count,
|
||||||
|
|
|
@ -4,11 +4,9 @@
|
||||||
|
|
||||||
defmodule Pleroma.User.Search do
|
defmodule Pleroma.User.Search do
|
||||||
alias Pleroma.Pagination
|
alias Pleroma.Pagination
|
||||||
alias Pleroma.Repo
|
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
@similarity_threshold 0.25
|
|
||||||
@limit 20
|
@limit 20
|
||||||
|
|
||||||
def search(query_string, opts \\ []) do
|
def search(query_string, opts \\ []) do
|
||||||
|
@ -23,18 +21,10 @@ def search(query_string, opts \\ []) do
|
||||||
|
|
||||||
maybe_resolve(resolve, for_user, query_string)
|
maybe_resolve(resolve, for_user, query_string)
|
||||||
|
|
||||||
{:ok, results} =
|
results =
|
||||||
Repo.transaction(fn ->
|
query_string
|
||||||
Ecto.Adapters.SQL.query(
|
|> search_query(for_user, following)
|
||||||
Repo,
|
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
|
||||||
"select set_limit(#{@similarity_threshold})",
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
query_string
|
|
||||||
|> search_query(for_user, following)
|
|
||||||
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
|
|
||||||
end)
|
|
||||||
|
|
||||||
results
|
results
|
||||||
end
|
end
|
||||||
|
@ -56,15 +46,65 @@ defp search_query(query_string, for_user, following) do
|
||||||
|> base_query(following)
|
|> base_query(following)
|
||||||
|> filter_blocked_user(for_user)
|
|> filter_blocked_user(for_user)
|
||||||
|> filter_blocked_domains(for_user)
|
|> filter_blocked_domains(for_user)
|
||||||
|> search_subqueries(query_string)
|
|> fts_search(query_string)
|
||||||
|> union_subqueries
|
|> trigram_rank(query_string)
|
||||||
|> distinct_query()
|
|> boost_search_rank(for_user)
|
||||||
|> boost_search_rank_query(for_user)
|
|
||||||
|> subquery()
|
|> subquery()
|
||||||
|> order_by(desc: :search_rank)
|
|> order_by(desc: :search_rank)
|
||||||
|> maybe_restrict_local(for_user)
|
|> maybe_restrict_local(for_user)
|
||||||
end
|
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, false), do: User
|
||||||
defp base_query(user, true), do: User.get_followers_query(user)
|
defp base_query(user, true), do: User.get_followers_query(user)
|
||||||
|
|
||||||
|
@ -87,21 +127,6 @@ defp filter_blocked_domains(query, %User{info: %{domain_blocks: domain_blocks}})
|
||||||
|
|
||||||
defp filter_blocked_domains(query, _), do: query
|
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
|
defp maybe_resolve(true, user, query) do
|
||||||
case {limit(), user} do
|
case {limit(), user} do
|
||||||
{:all, _} -> :noop
|
{:all, _} -> :noop
|
||||||
|
@ -126,9 +151,9 @@ defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauth
|
||||||
|
|
||||||
defp restrict_local(q), do: where(q, [u], u.local == true)
|
defp 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)
|
friends_ids = User.get_friends_ids(for_user)
|
||||||
followers_ids = User.get_followers_ids(for_user)
|
followers_ids = User.get_followers_ids(for_user)
|
||||||
|
|
||||||
|
@ -137,8 +162,8 @@ defp boost_search_rank_query(query, for_user) do
|
||||||
search_rank:
|
search_rank:
|
||||||
fragment(
|
fragment(
|
||||||
"""
|
"""
|
||||||
CASE WHEN (?) THEN 0.5 + (?) * 1.3
|
CASE WHEN (?) THEN (?) * 1.5
|
||||||
WHEN (?) THEN 0.5 + (?) * 1.2
|
WHEN (?) THEN (?) * 1.3
|
||||||
WHEN (?) THEN (?) * 1.1
|
WHEN (?) THEN (?) * 1.1
|
||||||
ELSE (?) END
|
ELSE (?) END
|
||||||
""",
|
""",
|
||||||
|
@ -154,70 +179,5 @@ defp boost_search_rank_query(query, for_user) do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
|
defp boost_search_rank(query, _for_user), do: query
|
||||||
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])
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
alias Pleroma.Activity.Ir.Topics
|
alias Pleroma.Activity.Ir.Topics
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
alias Pleroma.Conversation
|
alias Pleroma.Conversation
|
||||||
|
alias Pleroma.Conversation.Participation
|
||||||
alias Pleroma.Notification
|
alias Pleroma.Notification
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Object.Containment
|
alias Pleroma.Object.Containment
|
||||||
|
@ -131,7 +132,7 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
|
||||||
{:ok, map} <- MRF.filter(map),
|
{:ok, map} <- MRF.filter(map),
|
||||||
{recipients, _, _} = get_recipients(map),
|
{recipients, _, _} = get_recipients(map),
|
||||||
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
|
{: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, map, object} <- insert_full_object(map) do
|
||||||
{:ok, activity} =
|
{:ok, activity} =
|
||||||
Repo.insert(%Activity{
|
Repo.insert(%Activity{
|
||||||
|
@ -153,11 +154,8 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
|
||||||
|
|
||||||
Notification.create_notifications(activity)
|
Notification.create_notifications(activity)
|
||||||
|
|
||||||
participations =
|
conversation = create_or_bump_conversation(activity, map["actor"])
|
||||||
activity
|
participations = get_participations(conversation)
|
||||||
|> Conversation.create_or_bump_for()
|
|
||||||
|> get_participations()
|
|
||||||
|
|
||||||
stream_out(activity)
|
stream_out(activity)
|
||||||
stream_out_participations(participations)
|
stream_out_participations(participations)
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
|
@ -182,7 +180,20 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
|
||||||
end
|
end
|
||||||
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: []
|
defp get_participations(_), do: []
|
||||||
|
|
||||||
def stream_out_participations(participations) do
|
def stream_out_participations(participations) do
|
||||||
|
@ -225,6 +236,7 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f
|
||||||
# only accept false as false value
|
# only accept false as false value
|
||||||
local = !(params[:local] == false)
|
local = !(params[:local] == false)
|
||||||
published = params[:published]
|
published = params[:published]
|
||||||
|
quick_insert? = Pleroma.Config.get([:env]) == :benchmark
|
||||||
|
|
||||||
with create_data <-
|
with create_data <-
|
||||||
make_create_data(
|
make_create_data(
|
||||||
|
@ -235,12 +247,16 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f
|
||||||
{:fake, false, activity} <- {:fake, fake, activity},
|
{:fake, false, activity} <- {:fake, fake, activity},
|
||||||
_ <- increase_replies_count_if_reply(create_data),
|
_ <- increase_replies_count_if_reply(create_data),
|
||||||
_ <- increase_poll_votes_if_vote(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
|
# Changing note count prior to enqueuing federation task in order to avoid
|
||||||
# race conditions on updating user.info
|
# race conditions on updating user.info
|
||||||
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
|
{:quick_insert, true, activity} ->
|
||||||
|
{:ok, activity}
|
||||||
|
|
||||||
{:fake, true, activity} ->
|
{:fake, true, activity} ->
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
|
|
||||||
|
@ -1203,7 +1219,9 @@ def fetch_and_prepare_user_from_ap_id(ap_id) do
|
||||||
data <- maybe_update_follow_information(data) do
|
data <- maybe_update_follow_information(data) do
|
||||||
{:ok, data}
|
{:ok, data}
|
||||||
else
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ defp recipients(actor, activity) do
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers ++ fetchers
|
Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_cc_ap_ids(ap_id, recipients) do
|
defp get_cc_ap_ids(ap_id, recipients) do
|
||||||
|
|
|
@ -51,6 +51,20 @@ def publish(%Activity{data: %{"type" => "Create"}} = activity) do
|
||||||
|
|
||||||
def publish(_), do: {:error, "Not implemented"}
|
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, error}), do: format_error(error)
|
||||||
|
|
||||||
defp format_error(error) do
|
defp format_error(error) do
|
||||||
|
|
|
@ -1073,8 +1073,6 @@ def perform(:user_upgrade, user) do
|
||||||
|
|
||||||
Repo.update_all(q, [])
|
Repo.update_all(q, [])
|
||||||
|
|
||||||
maybe_retire_websub(user.ap_id)
|
|
||||||
|
|
||||||
q =
|
q =
|
||||||
from(
|
from(
|
||||||
a in Activity,
|
a in Activity,
|
||||||
|
@ -1117,19 +1115,6 @@ defp upgrade_user(user, data) do
|
||||||
|> User.update_and_set_cache()
|
|> User.update_and_set_cache()
|
||||||
end
|
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
|
def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
|
||||||
Map.put(data, "url", url["href"])
|
Map.put(data, "url", url["href"])
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,6 +46,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||||
:user_delete,
|
:user_delete,
|
||||||
:users_create,
|
:users_create,
|
||||||
:user_toggle_activation,
|
:user_toggle_activation,
|
||||||
|
:user_activate,
|
||||||
|
:user_deactivate,
|
||||||
:tag_users,
|
:tag_users,
|
||||||
:untag_users,
|
:untag_users,
|
||||||
:right_add,
|
:right_add,
|
||||||
|
@ -98,7 +100,7 @@ def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||||
|
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
actor: admin,
|
actor: admin,
|
||||||
subject: user,
|
subject: [user],
|
||||||
action: "delete"
|
action: "delete"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -106,6 +108,20 @@ def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||||
|> json(nickname)
|
|> json(nickname)
|
||||||
end
|
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, %{
|
def user_follow(%{assigns: %{user: admin}} = conn, %{
|
||||||
"follower" => follower_nick,
|
"follower" => follower_nick,
|
||||||
"followed" => followed_nick
|
"followed" => followed_nick
|
||||||
|
@ -240,7 +256,7 @@ def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => ni
|
||||||
|
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
actor: admin,
|
actor: admin,
|
||||||
subject: user,
|
subject: [user],
|
||||||
action: action
|
action: action
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -249,6 +265,36 @@ def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => ni
|
||||||
|> render("show.json", %{user: updated_user})
|
|> render("show.json", %{user: updated_user})
|
||||||
end
|
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
|
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||||
with {:ok, _} <- User.tag(nicknames, tags) do
|
with {:ok, _} <- User.tag(nicknames, tags) do
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
|
@ -313,6 +359,31 @@ defp maybe_parse_filters(filters) do
|
||||||
|> Enum.into(%{}, &{&1, true})
|
|> Enum.into(%{}, &{&1, true})
|
||||||
end
|
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, %{
|
def right_add(%{assigns: %{user: admin}} = conn, %{
|
||||||
"permission_group" => permission_group,
|
"permission_group" => permission_group,
|
||||||
"nickname" => nickname
|
"nickname" => nickname
|
||||||
|
@ -328,7 +399,7 @@ def right_add(%{assigns: %{user: admin}} = conn, %{
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
action: "grant",
|
action: "grant",
|
||||||
actor: admin,
|
actor: admin,
|
||||||
subject: user,
|
subject: [user],
|
||||||
permission: permission_group
|
permission: permission_group
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -349,8 +420,36 @@ def right_get(conn, %{"nickname" => nickname}) do
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
|
def right_delete_multiple(
|
||||||
render_error(conn, :forbidden, "You can't revoke your own admin status.")
|
%{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
|
end
|
||||||
|
|
||||||
def right_delete(
|
def right_delete(
|
||||||
|
@ -371,33 +470,24 @@ def right_delete(
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
action: "revoke",
|
action: "revoke",
|
||||||
actor: admin,
|
actor: admin,
|
||||||
subject: user,
|
subject: [user],
|
||||||
permission: permission_group
|
permission: permission_group
|
||||||
})
|
})
|
||||||
|
|
||||||
json(conn, info)
|
json(conn, info)
|
||||||
end
|
end
|
||||||
|
|
||||||
def right_delete(conn, _) do
|
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
|
||||||
render_error(conn, :not_found, "No such permission_group")
|
render_error(conn, :forbidden, "You can't revoke your own admin status.")
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_activation_status(%{assigns: %{user: admin}} = conn, %{
|
def relay_list(conn, _params) do
|
||||||
"nickname" => nickname,
|
with {:ok, list} <- Relay.list() do
|
||||||
"status" => status
|
json(conn, %{relays: list})
|
||||||
}) do
|
else
|
||||||
with {:ok, status} <- Ecto.Type.cast(:boolean, status),
|
_ ->
|
||||||
%User{} = user <- User.get_cached_by_nickname(nickname),
|
conn
|
||||||
{:ok, _} <- User.deactivate(user, !status) do
|
|> put_status(500)
|
||||||
action = if(user.info.deactivated, do: "activate", else: "deactivate")
|
|
||||||
|
|
||||||
ModerationLog.insert_log(%{
|
|
||||||
actor: admin,
|
|
||||||
subject: user,
|
|
||||||
action: action
|
|
||||||
})
|
|
||||||
|
|
||||||
json_response(conn, :no_content, "")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,12 @@ def render("index.json", %{users: users, count: count, page_size: page_size}) do
|
||||||
}
|
}
|
||||||
end
|
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
|
def render("show.json", %{user: user}) do
|
||||||
avatar = User.avatar_url(user) |> MediaProxy.url()
|
avatar = User.avatar_url(user) |> MediaProxy.url()
|
||||||
display_name = HTML.strip_tags(user.name || user.nickname)
|
display_name = HTML.strip_tags(user.name || user.nickname)
|
||||||
|
|
|
@ -10,19 +10,11 @@ defmodule Pleroma.Web.Federator do
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.Federator.Publisher
|
alias Pleroma.Web.Federator.Publisher
|
||||||
alias Pleroma.Web.OStatus
|
|
||||||
alias Pleroma.Web.Websub
|
|
||||||
alias Pleroma.Workers.PublisherWorker
|
alias Pleroma.Workers.PublisherWorker
|
||||||
alias Pleroma.Workers.ReceiverWorker
|
alias Pleroma.Workers.ReceiverWorker
|
||||||
alias Pleroma.Workers.SubscriberWorker
|
|
||||||
|
|
||||||
require Logger
|
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)"
|
@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
|
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
||||||
def allowed_incoming_reply_depth?(depth) do
|
def allowed_incoming_reply_depth?(depth) do
|
||||||
|
@ -37,10 +29,6 @@ def allowed_incoming_reply_depth?(depth) do
|
||||||
|
|
||||||
# Client API
|
# Client API
|
||||||
|
|
||||||
def incoming_doc(doc) do
|
|
||||||
ReceiverWorker.enqueue("incoming_doc", %{"body" => doc})
|
|
||||||
end
|
|
||||||
|
|
||||||
def incoming_ap_doc(params) do
|
def incoming_ap_doc(params) do
|
||||||
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params})
|
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params})
|
||||||
end
|
end
|
||||||
|
@ -53,18 +41,6 @@ def publish(activity) do
|
||||||
PublisherWorker.enqueue("publish", %{"activity_id" => activity.id})
|
PublisherWorker.enqueue("publish", %{"activity_id" => activity.id})
|
||||||
end
|
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
|
# Job Worker Callbacks
|
||||||
|
|
||||||
@spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
|
@spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
|
||||||
|
@ -81,11 +57,6 @@ def perform(:publish, activity) do
|
||||||
end
|
end
|
||||||
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
|
def perform(:incoming_ap_doc, params) do
|
||||||
Logger.info("Handling incoming AP activity")
|
Logger.info("Handling incoming AP activity")
|
||||||
|
|
||||||
|
@ -111,29 +82,6 @@ def perform(:incoming_ap_doc, params) do
|
||||||
end
|
end
|
||||||
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
|
def ap_enabled_actor(id) do
|
||||||
user = User.get_cached_by_ap_id(id)
|
user = User.get_cached_by_ap_id(id)
|
||||||
|
|
||||||
|
|
|
@ -80,4 +80,30 @@ def gather_nodeinfo_protocol_names do
|
||||||
links ++ module.gather_nodeinfo_protocol_names()
|
links ++ module.gather_nodeinfo_protocol_names()
|
||||||
end)
|
end)
|
||||||
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
|
end
|
||||||
|
|
|
@ -35,6 +35,13 @@ def init(%{qs: qs} = req, state) do
|
||||||
{_, stream} <- List.keyfind(params, "stream", 0),
|
{_, stream} <- List.keyfind(params, "stream", 0),
|
||||||
{:ok, user} <- allow_request(stream, [access_token, sec_websocket]),
|
{:ok, user} <- allow_request(stream, [access_token, sec_websocket]),
|
||||||
topic when is_binary(topic) <- expand_topic(stream, params) do
|
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}}
|
{:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
|
||||||
else
|
else
|
||||||
{:error, code} ->
|
{:error, code} ->
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -13,19 +13,14 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
||||||
alias Pleroma.Web.ActivityPub.ObjectView
|
alias Pleroma.Web.ActivityPub.ObjectView
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
alias Pleroma.Web.Endpoint
|
alias Pleroma.Web.Endpoint
|
||||||
alias Pleroma.Web.Federator
|
|
||||||
alias Pleroma.Web.Metadata.PlayerView
|
alias Pleroma.Web.Metadata.PlayerView
|
||||||
alias Pleroma.Web.OStatus.ActivityRepresenter
|
|
||||||
alias Pleroma.Web.Router
|
alias Pleroma.Web.Router
|
||||||
alias Pleroma.Web.XML
|
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
Pleroma.Plugs.RateLimiter,
|
Pleroma.Plugs.RateLimiter,
|
||||||
{:ap_routes, params: ["uuid"]} when action in [:object, :activity]
|
{:ap_routes, params: ["uuid"]} when action in [:object, :activity]
|
||||||
)
|
)
|
||||||
|
|
||||||
plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
|
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
Pleroma.Plugs.SetFormatPlug
|
Pleroma.Plugs.SetFormatPlug
|
||||||
when action in [:object, :activity, :notice]
|
when action in [:object, :activity, :notice]
|
||||||
|
@ -33,32 +28,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
||||||
|
|
||||||
action_fallback(:errors)
|
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})
|
def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
|
||||||
when format in ["json", "activity+json"] do
|
when format in ["json", "activity+json"] do
|
||||||
ActivityPubController.call(conn, :object)
|
ActivityPubController.call(conn, :object)
|
||||||
|
@ -179,23 +148,10 @@ defp represent_activity(
|
||||||
|> render("object.json", %{object: object})
|
|> render("object.json", %{object: object})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp represent_activity(_conn, "activity+json", _, _) do
|
defp represent_activity(_conn, _, _, _) do
|
||||||
{:error, :not_found}
|
{:error, :not_found}
|
||||||
end
|
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
|
def errors(conn, {:error, :not_found}) do
|
||||||
render_error(conn, :not_found, "Not found")
|
render_error(conn, :not_found, "Not found")
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
|
@ -137,11 +137,14 @@ defmodule Pleroma.Web.Router do
|
||||||
delete("/users", AdminAPIController, :user_delete)
|
delete("/users", AdminAPIController, :user_delete)
|
||||||
post("/users", AdminAPIController, :users_create)
|
post("/users", AdminAPIController, :users_create)
|
||||||
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
|
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)
|
put("/users/tag", AdminAPIController, :tag_users)
|
||||||
delete("/users/tag", AdminAPIController, :untag_users)
|
delete("/users/tag", AdminAPIController, :untag_users)
|
||||||
|
|
||||||
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
|
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
|
||||||
get("/users/:nickname/permission_group/: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)
|
post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
|
||||||
|
|
||||||
delete(
|
delete(
|
||||||
|
@ -150,8 +153,15 @@ defmodule Pleroma.Web.Router do
|
||||||
:right_delete
|
: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)
|
post("/relay", AdminAPIController, :relay_follow)
|
||||||
delete("/relay", AdminAPIController, :relay_unfollow)
|
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", Feed.FeedController, :feed)
|
||||||
get("/users/:nickname", Feed.FeedController, :feed_redirect)
|
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)
|
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -49,7 +49,7 @@ defp handle_should_send(:test) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_should_send(_) do
|
defp handle_should_send(:benchmark), do: false
|
||||||
true
|
|
||||||
end
|
defp handle_should_send(_), do: true
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,8 +10,6 @@
|
||||||
<title><%= @user.nickname <> "'s timeline" %></title>
|
<title><%= @user.nickname <> "'s timeline" %></title>
|
||||||
<updated><%= most_recent_update(@activities, @user) %></updated>
|
<updated><%= most_recent_update(@activities, @user) %></updated>
|
||||||
<logo><%= logo(@user) %></logo>
|
<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"/>
|
<link rel="self" href="<%= '#{feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/>
|
||||||
|
|
||||||
<%= render @view_module, "_author.xml", assigns %>
|
<%= render @view_module, "_author.xml", assigns %>
|
||||||
|
|
|
@ -108,7 +108,6 @@ defp webfinger_from_xml(doc) do
|
||||||
doc
|
doc
|
||||||
),
|
),
|
||||||
subject <- XML.string_from_xpath("//Subject", doc),
|
subject <- XML.string_from_xpath("//Subject", doc),
|
||||||
salmon <- XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc),
|
|
||||||
subscribe_address <-
|
subscribe_address <-
|
||||||
XML.string_from_xpath(
|
XML.string_from_xpath(
|
||||||
~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
|
~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
|
||||||
|
@ -123,7 +122,6 @@ defp webfinger_from_xml(doc) do
|
||||||
"magic_key" => magic_key,
|
"magic_key" => magic_key,
|
||||||
"topic" => topic,
|
"topic" => topic,
|
||||||
"subject" => subject,
|
"subject" => subject,
|
||||||
"salmon" => salmon,
|
|
||||||
"subscribe_address" => subscribe_address,
|
"subscribe_address" => subscribe_address,
|
||||||
"ap_id" => ap_id
|
"ap_id" => ap_id
|
||||||
}
|
}
|
||||||
|
@ -148,16 +146,6 @@ defp webfinger_from_json(doc) do
|
||||||
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
|
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
|
||||||
Map.put(data, "ap_id", link["href"])
|
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"} ->
|
{_, "http://ostatus.org/schema/1.0/subscribe"} ->
|
||||||
Map.put(data, "subscribe_address", link["template"])
|
Map.put(data, "subscribe_address", link["template"])
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -8,10 +8,6 @@ defmodule Pleroma.Workers.ReceiverWorker do
|
||||||
use Pleroma.Workers.WorkerHelper, queue: "federator_incoming"
|
use Pleroma.Workers.WorkerHelper, queue: "federator_incoming"
|
||||||
|
|
||||||
@impl Oban.Worker
|
@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
|
def perform(%{"op" => "incoming_ap_doc", "params" => params}, _job) do
|
||||||
Federator.perform(:incoming_ap_doc, params)
|
Federator.perform(:incoming_ap_doc, params)
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
mix.exs
1
mix.exs
|
@ -69,6 +69,7 @@ def application do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specifies which paths to compile per environment.
|
# 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(:test), do: ["lib", "test/support"]
|
||||||
defp elixirc_paths(_), do: ["lib"]
|
defp elixirc_paths(_), do: ["lib"]
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -4,7 +4,7 @@ defmodule Pleroma.Repo.Migrations.CopyMutedToMutedNotifications do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
execute(
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -141,8 +141,8 @@ else
|
||||||
|
|
||||||
ACTION="$1"
|
ACTION="$1"
|
||||||
shift
|
shift
|
||||||
|
echo "$1" | grep "^-" >/dev/null
|
||||||
if [ "$(echo \"$1\" | grep \"^-\" >/dev/null)" = false ]; then
|
if [ $? -eq 1 ]; then
|
||||||
SUBACTION="$1"
|
SUBACTION="$1"
|
||||||
shift
|
shift
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -23,6 +23,39 @@ test "getting a participation will also preload things" do
|
||||||
assert %Pleroma.Conversation{} = participation.conversation
|
assert %Pleroma.Conversation{} = participation.conversation
|
||||||
end
|
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
|
test "for a new conversation, it sets the recipents of the participation" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
other_user = insert(:user)
|
other_user = insert(:user)
|
||||||
|
@ -32,7 +65,7 @@ test "for a new conversation, it sets the recipents of the participation" do
|
||||||
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
|
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
|
||||||
|
|
||||||
user = User.get_cached_by_id(user.id)
|
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] = Participation.for_user(user)
|
||||||
participation = Pleroma.Repo.preload(participation, :recipients)
|
participation = Pleroma.Repo.preload(participation, :recipients)
|
||||||
|
|
||||||
|
|
1
test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json
vendored
Normal file
1
test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json
vendored
Normal 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
test/fixtures/tesla_mock/moonman@shitposter.club.json
vendored
Normal file
1
test/fixtures/tesla_mock/moonman@shitposter.club.json
vendored
Normal 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"}
|
|
@ -24,13 +24,13 @@ test "logging user deletion by moderator", %{moderator: moderator, subject1: sub
|
||||||
{:ok, _} =
|
{:ok, _} =
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
actor: moderator,
|
actor: moderator,
|
||||||
subject: subject1,
|
subject: [subject1],
|
||||||
action: "delete"
|
action: "delete"
|
||||||
})
|
})
|
||||||
|
|
||||||
log = Repo.one(ModerationLog)
|
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
|
end
|
||||||
|
|
||||||
test "logging user creation by moderator", %{
|
test "logging user creation by moderator", %{
|
||||||
|
@ -128,7 +128,7 @@ test "logging user grant by moderator", %{moderator: moderator, subject1: subjec
|
||||||
{:ok, _} =
|
{:ok, _} =
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
actor: moderator,
|
actor: moderator,
|
||||||
subject: subject1,
|
subject: [subject1],
|
||||||
action: "grant",
|
action: "grant",
|
||||||
permission: "moderator"
|
permission: "moderator"
|
||||||
})
|
})
|
||||||
|
@ -142,7 +142,7 @@ test "logging user revoke by moderator", %{moderator: moderator, subject1: subje
|
||||||
{:ok, _} =
|
{:ok, _} =
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
actor: moderator,
|
actor: moderator,
|
||||||
subject: subject1,
|
subject: [subject1],
|
||||||
action: "revoke",
|
action: "revoke",
|
||||||
permission: "moderator"
|
permission: "moderator"
|
||||||
})
|
})
|
||||||
|
|
|
@ -65,7 +65,7 @@ test "users cannot be collided through fake direction spoofing attempts" do
|
||||||
assert capture_log(fn ->
|
assert capture_log(fn ->
|
||||||
{:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
|
{:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
|
||||||
end) =~
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -27,31 +27,16 @@ defmodule Pleroma.Object.FetcherTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "actor origin containment" do
|
describe "actor origin containment" do
|
||||||
test_with_mock "it rejects objects with a bogus origin",
|
test "it rejects objects with a bogus origin" do
|
||||||
Pleroma.Web.OStatus,
|
|
||||||
[:passthrough],
|
|
||||||
[] do
|
|
||||||
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
|
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
|
||||||
|
|
||||||
refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test_with_mock "it rejects objects when attributedTo is wrong (variant 1)",
|
test "it rejects objects when attributedTo is wrong (variant 1)" do
|
||||||
Pleroma.Web.OStatus,
|
|
||||||
[:passthrough],
|
|
||||||
[] do
|
|
||||||
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
|
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
|
||||||
|
|
||||||
refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test_with_mock "it rejects objects when attributedTo is wrong (variant 2)",
|
test "it rejects objects when attributedTo is wrong (variant 2)" do
|
||||||
Pleroma.Web.OStatus,
|
|
||||||
[:passthrough],
|
|
||||||
[] do
|
|
||||||
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
|
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
|
||||||
|
|
||||||
refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -71,24 +56,6 @@ test "it fetches an object" do
|
||||||
|
|
||||||
assert object == object_again
|
assert object == object_again
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "implementation quirks" do
|
describe "implementation quirks" do
|
||||||
|
|
12
test/safe_jsonb_set_test.exs
Normal file
12
test/safe_jsonb_set_test.exs
Normal 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
|
|
@ -69,8 +69,7 @@ test "it returns key" do
|
||||||
|
|
||||||
test "it returns error when not found user" do
|
test "it returns error when not found user" do
|
||||||
assert capture_log(fn ->
|
assert capture_log(fn ->
|
||||||
assert Signature.refetch_public_key(make_fake_conn("test-ap_id")) ==
|
{:error, _} = Signature.refetch_public_key(make_fake_conn("test-ap_id"))
|
||||||
{:error, {:error, :ok}}
|
|
||||||
end) =~ "[error] Could not decode user"
|
end) =~ "[error] Could not decode user"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -281,26 +281,6 @@ def follow_activity_factory do
|
||||||
}
|
}
|
||||||
end
|
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
|
def oauth_app_factory do
|
||||||
%Pleroma.Web.OAuth.App{
|
%Pleroma.Web.OAuth.App{
|
||||||
client_name: "Some client",
|
client_name: "Some client",
|
||||||
|
|
|
@ -38,6 +38,14 @@ def get("https://osada.macgirvin.com/channel/mike", _, _, _) do
|
||||||
}}
|
}}
|
||||||
end
|
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
|
def get("https://mastodon.social/users/emelie/statuses/101849165031453009", _, _, _) do
|
||||||
{:ok,
|
{:ok,
|
||||||
%Tesla.Env{
|
%Tesla.Env{
|
||||||
|
@ -620,7 +628,7 @@ def get("https://shitposter.club/notice/2827873", _, _, _) do
|
||||||
{:ok,
|
{:ok,
|
||||||
%Tesla.Env{
|
%Tesla.Env{
|
||||||
status: 200,
|
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
|
end
|
||||||
|
|
||||||
|
|
|
@ -65,21 +65,6 @@ test "finds users, considering density of matched tokens" do
|
||||||
assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
|
assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
|
||||||
end
|
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
|
test "finds users, boosting ranks of friends and followers" do
|
||||||
u1 = insert(:user)
|
u1 = insert(:user)
|
||||||
u2 = insert(:user, %{name: "Doe"})
|
u2 = insert(:user, %{name: "Doe"})
|
||||||
|
@ -163,17 +148,6 @@ test "find all users for unauthenticated users when `limit_to_local_content` is
|
||||||
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
|
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
|
||||||
end
|
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
|
test "does not yield false-positive matches" do
|
||||||
insert(:user, %{name: "John Doe"})
|
insert(:user, %{name: "John Doe"})
|
||||||
|
|
||||||
|
|
|
@ -190,23 +190,6 @@ test "local users do not automatically follow local locked accounts" do
|
||||||
refute User.following?(follower, followed)
|
refute User.following?(follower, followed)
|
||||||
end
|
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
|
describe "unfollow/2" do
|
||||||
setup do
|
setup do
|
||||||
setting = Pleroma.Config.get([:instance, :external_user_synchronization])
|
setting = Pleroma.Config.get([:instance, :external_user_synchronization])
|
||||||
|
@ -474,11 +457,6 @@ test "gets an existing user by fully qualified nickname, case insensitive" do
|
||||||
assert user == fetched_user
|
assert user == fetched_user
|
||||||
end
|
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
|
test "returns nil if no user could be fetched" do
|
||||||
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
|
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
|
||||||
assert fetched_user == "not found nonexistant@social.heldscal.la"
|
assert fetched_user == "not found nonexistant@social.heldscal.la"
|
||||||
|
|
|
@ -41,6 +41,27 @@ test "it streams them out" do
|
||||||
assert called(Pleroma.Web.Streamer.stream("participation", participations))
|
assert called(Pleroma.Web.Streamer.stream("participation", participations))
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
describe "fetching restricted by visibility" do
|
describe "fetching restricted by visibility" do
|
||||||
|
|
|
@ -22,8 +22,8 @@ test "gets an actor for the relay" do
|
||||||
describe "follow/1" do
|
describe "follow/1" do
|
||||||
test "returns errors when user not found" do
|
test "returns errors when user not found" do
|
||||||
assert capture_log(fn ->
|
assert capture_log(fn ->
|
||||||
assert Relay.follow("test-ap-id") == {:error, "Could not fetch by AP id"}
|
{:error, _} = Relay.follow("test-ap-id")
|
||||||
end) =~ "Could not fetch by AP id"
|
end) =~ "Could not decode user at fetch"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns activity" do
|
test "returns activity" do
|
||||||
|
@ -41,8 +41,8 @@ test "returns activity" do
|
||||||
describe "unfollow/1" do
|
describe "unfollow/1" do
|
||||||
test "returns errors when user not found" do
|
test "returns errors when user not found" do
|
||||||
assert capture_log(fn ->
|
assert capture_log(fn ->
|
||||||
assert Relay.unfollow("test-ap-id") == {:error, "Could not fetch by AP id"}
|
{:error, _} = Relay.unfollow("test-ap-id")
|
||||||
end) =~ "Could not fetch by AP id"
|
end) =~ "Could not decode user at fetch"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns activity" do
|
test "returns activity" do
|
||||||
|
|
|
@ -7,14 +7,11 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Object.Fetcher
|
alias Pleroma.Object.Fetcher
|
||||||
alias Pleroma.Repo
|
|
||||||
alias Pleroma.Tests.ObanHelpers
|
alias Pleroma.Tests.ObanHelpers
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.OStatus
|
|
||||||
alias Pleroma.Web.Websub.WebsubClientSubscription
|
|
||||||
|
|
||||||
import Mock
|
import Mock
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
|
@ -1181,32 +1178,6 @@ test "it sets the 'attributedTo' property to the actor of the object if it doesn
|
||||||
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
|
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
|
||||||
end
|
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
|
test "it strips internal hashtag data" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
|
||||||
|
@ -1371,21 +1342,6 @@ test "it upgrades a user to activitypub" do
|
||||||
end
|
end
|
||||||
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
|
describe "actor rewriting" do
|
||||||
test "it fixes the actor URL property to be a proper URI" do
|
test "it fixes the actor URL property to be a proper URI" do
|
||||||
data = %{
|
data = %{
|
||||||
|
|
|
@ -17,8 +17,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|
||||||
alias Pleroma.Web.MediaProxy
|
alias Pleroma.Web.MediaProxy
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
|
|
||||||
describe "/api/pleroma/admin/users" do
|
setup_all do
|
||||||
test "Delete" 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})
|
admin = insert(:user, info: %{is_admin: true})
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
|
||||||
|
@ -30,15 +36,36 @@ test "Delete" do
|
||||||
|
|
||||||
log_entry = Repo.one(ModerationLog)
|
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) ==
|
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
|
assert json_response(conn, 200) == user.nickname
|
||||||
end
|
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
|
test "Create" do
|
||||||
admin = insert(:user, info: %{is_admin: true})
|
admin = insert(:user, info: %{is_admin: true})
|
||||||
|
|
||||||
|
@ -404,6 +431,29 @@ test "/:right POST, can add to a permission group" do
|
||||||
"@#{admin.nickname} made @#{user.nickname} admin"
|
"@#{admin.nickname} made @#{user.nickname} admin"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "/:right POST, can add to a permission group (multiple)" 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")
|
||||||
|
|> post("/api/pleroma/admin/users/permission_group/admin", %{
|
||||||
|
nicknames: [user_one.nickname, user_two.nickname]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert json_response(conn, 200) == %{
|
||||||
|
"is_admin" => true
|
||||||
|
}
|
||||||
|
|
||||||
|
log_entry = Repo.one(ModerationLog)
|
||||||
|
|
||||||
|
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||||
|
"@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
|
||||||
|
end
|
||||||
|
|
||||||
test "/:right DELETE, can remove from a permission group" do
|
test "/:right DELETE, can remove from a permission group" do
|
||||||
admin = insert(:user, info: %{is_admin: true})
|
admin = insert(:user, info: %{is_admin: true})
|
||||||
user = insert(:user, info: %{is_admin: true})
|
user = insert(:user, info: %{is_admin: true})
|
||||||
|
@ -423,63 +473,30 @@ test "/:right DELETE, can remove from a permission group" do
|
||||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||||
"@#{admin.nickname} revoked admin role from @#{user.nickname}"
|
"@#{admin.nickname} revoked admin role from @#{user.nickname}"
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do
|
test "/:right DELETE, can remove from a permission group (multiple)" do
|
||||||
setup %{conn: conn} do
|
|
||||||
admin = insert(:user, info: %{is_admin: true})
|
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 =
|
||||||
conn
|
build_conn()
|
||||||
|> assign(:user, admin)
|
|> assign(:user, admin)
|
||||||
|> put_req_header("accept", "application/json")
|
|> put_req_header("accept", "application/json")
|
||||||
|
|> delete("/api/pleroma/admin/users/permission_group/admin", %{
|
||||||
|
nicknames: [user_one.nickname, user_two.nickname]
|
||||||
|
})
|
||||||
|
|
||||||
%{conn: conn, admin: admin}
|
assert json_response(conn, 200) == %{
|
||||||
end
|
"is_admin" => false
|
||||||
|
}
|
||||||
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)
|
|
||||||
|
|
||||||
log_entry = Repo.one(ModerationLog)
|
log_entry = Repo.one(ModerationLog)
|
||||||
|
|
||||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||||
"@#{admin.nickname} deactivated user @#{user.nickname}"
|
"@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{
|
||||||
end
|
user_two.nickname
|
||||||
|
}"
|
||||||
test "activates the user", %{conn: conn, admin: admin} do
|
|
||||||
user = insert(:user, info: %{deactivated: true})
|
|
||||||
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true})
|
|
||||||
|
|
||||||
user = User.get_cached_by_id(user.id)
|
|
||||||
assert user.info.deactivated == false
|
|
||||||
assert json_response(conn, :no_content)
|
|
||||||
|
|
||||||
log_entry = Repo.one(ModerationLog)
|
|
||||||
|
|
||||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
|
||||||
"@#{admin.nickname} activated user @#{user.nickname}"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns 403 when requested by a non-admin", %{conn: conn} do
|
|
||||||
user = insert(:user)
|
|
||||||
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> assign(:user, user)
|
|
||||||
|> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
|
|
||||||
|
|
||||||
assert json_response(conn, :forbidden)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1029,6 +1046,50 @@ test "it works with multiple filters" do
|
||||||
end
|
end
|
||||||
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
|
test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
|
||||||
admin = insert(:user, info: %{is_admin: true})
|
admin = insert(:user, info: %{is_admin: true})
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
@ -1053,7 +1114,7 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
|
||||||
log_entry = Repo.one(ModerationLog)
|
log_entry = Repo.one(ModerationLog)
|
||||||
|
|
||||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||||
"@#{admin.nickname} deactivated user @#{user.nickname}"
|
"@#{admin.nickname} deactivated users: @#{user.nickname}"
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "POST /api/pleroma/admin/users/invite_token" do
|
describe "POST /api/pleroma/admin/users/invite_token" do
|
||||||
|
@ -2486,6 +2547,74 @@ test "sets password_reset_pending to true", %{admin: admin, user: user} do
|
||||||
assert User.get_by_id(user.id).info.password_reset_pending == true
|
assert User.get_by_id(user.id).info.password_reset_pending == true
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
# Needed for testing
|
# Needed for testing
|
||||||
|
|
|
@ -111,93 +111,6 @@ test "it federates only to reachable instances via AP" do
|
||||||
all_enqueued(worker: PublisherWorker)
|
all_enqueued(worker: PublisherWorker)
|
||||||
)
|
)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "Receive an activity" do
|
describe "Receive an activity" do
|
||||||
|
|
|
@ -54,9 +54,9 @@ test "returns a list of conversations", %{conn: conn} do
|
||||||
assert user_two.id in account_ids
|
assert user_two.id in account_ids
|
||||||
assert user_three.id in account_ids
|
assert user_three.id in account_ids
|
||||||
assert is_binary(res_id)
|
assert is_binary(res_id)
|
||||||
assert unread == true
|
assert unread == false
|
||||||
assert res_last_status["id"] == direct.id
|
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
|
end
|
||||||
|
|
||||||
test "updates the last_status on reply", %{conn: conn} do
|
test "updates the last_status on reply", %{conn: conn} do
|
||||||
|
@ -95,19 +95,23 @@ test "the user marks a conversation as read", %{conn: conn} do
|
||||||
"visibility" => "direct"
|
"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}] =
|
[%{"id" => direct_conversation_id, "unread" => true}] =
|
||||||
conn
|
conn
|
||||||
|> assign(:user, user_one)
|
|> assign(:user, user_two)
|
||||||
|> get("/api/v1/conversations")
|
|> get("/api/v1/conversations")
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
||||||
%{"unread" => false} =
|
%{"unread" => false} =
|
||||||
conn
|
conn
|
||||||
|> assign(:user, user_one)
|
|> assign(:user, user_two)
|
||||||
|> post("/api/v1/conversations/#{direct_conversation_id}/read")
|
|> post("/api/v1/conversations/#{direct_conversation_id}/read")
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
||||||
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 0
|
assert User.get_cached_by_id(user_one.id).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
|
# The conversation is marked as unread on reply
|
||||||
{:ok, _} =
|
{:ok, _} =
|
||||||
|
@ -124,6 +128,7 @@ test "the user marks a conversation as read", %{conn: conn} do
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
||||||
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
|
assert User.get_cached_by_id(user_one.id).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
|
# A reply doesn't increment the user's unread_conversation_count if the conversation is unread
|
||||||
{:ok, _} =
|
{:ok, _} =
|
||||||
|
@ -134,6 +139,7 @@ test "the user marks a conversation as read", %{conn: conn} do
|
||||||
})
|
})
|
||||||
|
|
||||||
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 == 1
|
||||||
|
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do
|
test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do
|
||||||
|
|
|
@ -204,17 +204,17 @@ test "search fetches remote accounts", %{conn: conn} do
|
||||||
conn =
|
conn =
|
||||||
conn
|
conn
|
||||||
|> assign(:user, user)
|
|> 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)
|
assert results = json_response(conn, 200)
|
||||||
[account] = results["accounts"]
|
[account] = results["accounts"]
|
||||||
assert account["acct"] == "shp@social.heldscal.la"
|
assert account["acct"] == "mike@osada.macgirvin.com"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do
|
test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do
|
||||||
conn =
|
conn =
|
||||||
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 = json_response(conn, 200)
|
||||||
assert [] == results["accounts"]
|
assert [] == results["accounts"]
|
||||||
|
|
|
@ -11,7 +11,6 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.OStatus
|
|
||||||
|
|
||||||
clear_config([:instance, :public])
|
clear_config([:instance, :public])
|
||||||
|
|
||||||
|
@ -75,8 +74,7 @@ test "the public timeline", %{conn: conn} do
|
||||||
|
|
||||||
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
|
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
|
||||||
|
|
||||||
{:ok, [_activity]} =
|
_activity = insert(:note_activity, local: false)
|
||||||
OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
|
|
||||||
|
|
||||||
conn = get(conn, "/api/v1/timelines/public", %{"local" => "False"})
|
conn = get(conn, "/api/v1/timelines/public", %{"local" => "False"})
|
||||||
|
|
||||||
|
@ -271,9 +269,6 @@ test "hashtag timeline", %{conn: conn} do
|
||||||
|
|
||||||
{:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"})
|
{: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")
|
nconn = get(conn, "/api/v1/timelines/tag/2hu")
|
||||||
|
|
||||||
assert [%{"id" => id}] = json_response(nconn, :ok)
|
assert [%{"id" => id}] = json_response(nconn, :ok)
|
||||||
|
|
|
@ -424,8 +424,8 @@ test "shows unread_conversation_count only to the account owner" do
|
||||||
other_user = insert(:user)
|
other_user = insert(:user)
|
||||||
|
|
||||||
{:ok, _activity} =
|
{:ok, _activity} =
|
||||||
CommonAPI.post(user, %{
|
CommonAPI.post(other_user, %{
|
||||||
"status" => "Hey @#{other_user.nickname}.",
|
"status" => "Hey @#{user.nickname}.",
|
||||||
"visibility" => "direct"
|
"visibility" => "direct"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
||||||
alias Pleroma.Web.CommonAPI.Utils
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
alias Pleroma.Web.MastodonAPI.AccountView
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
alias Pleroma.Web.OStatus
|
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
import Tesla.Mock
|
import Tesla.Mock
|
||||||
|
|
||||||
|
@ -230,17 +229,15 @@ test "a reply" do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "contains mentions" do
|
test "contains mentions" do
|
||||||
incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
|
user = insert(:user)
|
||||||
# a user with this ap id might be in the cache.
|
mentioned = insert(:user)
|
||||||
recipient = "https://pleroma.soykaf.com/users/lain"
|
|
||||||
user = insert(:user, %{ap_id: recipient})
|
|
||||||
|
|
||||||
{:ok, [activity]} = OStatus.handle_incoming(incoming)
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "hi @#{mentioned.nickname}"})
|
||||||
|
|
||||||
status = StatusView.render("show.json", %{activity: activity})
|
status = StatusView.render("show.json", %{activity: activity})
|
||||||
|
|
||||||
assert status.mentions ==
|
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
|
end
|
||||||
|
|
||||||
test "create mentions from the 'to' field" do
|
test "create mentions from the 'to' field" do
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -11,7 +11,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.OStatus.ActivityRepresenter
|
|
||||||
|
|
||||||
setup_all do
|
setup_all do
|
||||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
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)
|
Pleroma.Config.put([:instance, :federating], true)
|
||||||
end
|
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
|
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
|
test "redirects to /notice/id for html format", %{conn: conn} do
|
||||||
note_activity = insert(:note_activity)
|
note_activity = insert(:note_activity)
|
||||||
object = Object.normalize(note_activity)
|
object = Object.normalize(note_activity)
|
||||||
|
@ -143,16 +71,6 @@ test "404s on nonexisting objects", %{conn: conn} do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET activity/2" do
|
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
|
test "redirects to /notice/id for html format", %{conn: conn} do
|
||||||
note_activity = insert(:note_activity)
|
note_activity = insert(:note_activity)
|
||||||
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
|
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
|
||||||
|
@ -180,24 +98,6 @@ test "505s when user not found", %{conn: conn} do
|
||||||
assert response(conn, 500) == ~S({"error":"Something went wrong"})
|
assert response(conn, 500) == ~S({"error":"Something went wrong"})
|
||||||
end
|
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
|
test "404s on private activities", %{conn: conn} do
|
||||||
note_activity = insert(:direct_note_activity)
|
note_activity = insert(:direct_note_activity)
|
||||||
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
|
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -45,19 +45,6 @@ test "returns error when fails parse xml or json" do
|
||||||
assert {:error, %Jason.DecodeError{}} = WebFinger.finger(user)
|
assert {:error, %Jason.DecodeError{}} = WebFinger.finger(user)
|
||||||
end
|
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
|
test "returns the ActivityPub actor URI for an ActivityPub user" do
|
||||||
user = "framasoft@framatube.org"
|
user = "framasoft@framatube.org"
|
||||||
|
|
||||||
|
@ -72,20 +59,6 @@ test "returns the ActivityPub actor URI for an ActivityPub user with the ld+json
|
||||||
assert data["ap_id"] == "https://gerzilla.de/channel/kaniini"
|
assert data["ap_id"] == "https://gerzilla.de/channel/kaniini"
|
||||||
end
|
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
|
test "it work for AP-only user" do
|
||||||
user = "kpherox@mstdn.jp"
|
user = "kpherox@mstdn.jp"
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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…
Reference in a new issue