forked from AkkomaGang/akkoma
Merge branch 'develop' into refactor/following-relationships
This commit is contained in:
commit
4ea1a61b00
219 changed files with 3097 additions and 4626 deletions
|
@ -1,3 +1,3 @@
|
||||||
[
|
[
|
||||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/repo/migrations/*.exs"]
|
||||||
]
|
]
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -70,7 +101,7 @@ docs-deploy:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: alpine:latest
|
image: alpine:latest
|
||||||
only:
|
only:
|
||||||
- master@pleroma/pleroma
|
- stable@pleroma/pleroma
|
||||||
- develop@pleroma/pleroma
|
- develop@pleroma/pleroma
|
||||||
before_script:
|
before_script:
|
||||||
- apk add curl
|
- apk add curl
|
||||||
|
@ -127,9 +158,10 @@ amd64:
|
||||||
# TODO: Replace with upstream image when 1.9.0 comes out
|
# TODO: Replace with upstream image when 1.9.0 comes out
|
||||||
image: rinpatch/elixir:1.9.0-rc.0
|
image: rinpatch/elixir:1.9.0-rc.0
|
||||||
only: &release-only
|
only: &release-only
|
||||||
- master@pleroma/pleroma
|
- stable@pleroma/pleroma
|
||||||
- develop@pleroma/pleroma
|
- develop@pleroma/pleroma
|
||||||
- /^maint/.*$/@pleroma/pleroma
|
- /^maint/.*$/@pleroma/pleroma
|
||||||
|
- /^release/.*$/@pleroma/pleroma
|
||||||
artifacts: &release-artifacts
|
artifacts: &release-artifacts
|
||||||
name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME"
|
name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME"
|
||||||
paths:
|
paths:
|
||||||
|
|
101
CHANGELOG.md
101
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,8 +45,12 @@ 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
|
||||||
|
- Mastodon API: Add `exclude_visibilities` parameter to the timeline and notification endpoints
|
||||||
|
- Admin API: `/users/:nickname/toggle_activation` endpoint is now deprecated in favor of: `/users/activate`, `/users/deactivate`, both accept `nicknames` array
|
||||||
|
- Admin API: `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group` (both accept `nicknames` array), `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body).
|
||||||
|
- Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays
|
||||||
|
- Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
|
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
|
||||||
|
@ -27,14 +62,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Admin API: Return link alongside with token on password reset
|
- Admin API: Return link alongside with token on password reset
|
||||||
- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities
|
- 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: 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
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
@ -42,16 +101,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`
|
||||||
|
@ -60,7 +118,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
|
||||||
|
@ -71,21 +129,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.
|
||||||
|
@ -99,6 +164,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>
|
||||||
|
@ -107,7 +173,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
|
||||||
|
@ -125,11 +192,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,
|
||||||
|
@ -328,6 +322,16 @@
|
||||||
],
|
],
|
||||||
default_mascot: :pleroma_fox_tan
|
default_mascot: :pleroma_fox_tan
|
||||||
|
|
||||||
|
config :pleroma, :manifest,
|
||||||
|
icons: [
|
||||||
|
%{
|
||||||
|
src: "/static/logo.png",
|
||||||
|
type: "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
theme_color: "#282c37",
|
||||||
|
background_color: "#191b22"
|
||||||
|
|
||||||
config :pleroma, :activitypub,
|
config :pleroma, :activitypub,
|
||||||
unfollow_blocked: true,
|
unfollow_blocked: true,
|
||||||
outgoing_blocks: true,
|
outgoing_blocks: true,
|
||||||
|
|
|
@ -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
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
|
@ -1100,6 +1098,45 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
group: :pleroma,
|
||||||
|
key: :manifest,
|
||||||
|
type: :group,
|
||||||
|
description:
|
||||||
|
"This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE",
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :icons,
|
||||||
|
type: {:list, :map},
|
||||||
|
description: "Describe the icons of the app",
|
||||||
|
suggestion: [
|
||||||
|
%{
|
||||||
|
src: "/static/logo.png"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
src: "/static/icon.png",
|
||||||
|
type: "image/png"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
src: "/static/icon.ico",
|
||||||
|
sizes: "72x72 96x96 128x128 256x256"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :theme_color,
|
||||||
|
type: :string,
|
||||||
|
description: "Describe the theme color of the app",
|
||||||
|
suggestions: ["#282c37", "mediumpurple"]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :background_color,
|
||||||
|
type: :string,
|
||||||
|
description: "Describe the background color of the app",
|
||||||
|
suggestions: ["#191b22", "aliceblue"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :mrf_simple,
|
key: :mrf_simple,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -13,6 +13,7 @@ Some apps operate under the assumption that no more than 4 attachments can be re
|
||||||
## Timelines
|
## Timelines
|
||||||
|
|
||||||
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
|
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
|
||||||
|
Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`.
|
||||||
|
|
||||||
## Statuses
|
## Statuses
|
||||||
|
|
||||||
|
@ -84,6 +85,12 @@ Has these additional fields under the `pleroma` object:
|
||||||
|
|
||||||
- `is_seen`: true if the notification was read by the user
|
- `is_seen`: true if the notification was read by the user
|
||||||
|
|
||||||
|
## GET `/api/v1/notifications`
|
||||||
|
|
||||||
|
Accepts additional parameters:
|
||||||
|
|
||||||
|
- `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
|
||||||
|
|
||||||
## POST `/api/v1/statuses`
|
## POST `/api/v1/statuses`
|
||||||
|
|
||||||
Additional parameters can be added to the JSON body/Form data:
|
Additional parameters can be added to the JSON body/Form data:
|
||||||
|
|
|
@ -367,6 +367,13 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
|
||||||
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
|
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
|
||||||
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
|
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
|
||||||
|
|
||||||
|
## `GET /api/v1/pleroma/conversations/read`
|
||||||
|
### Marks all user's conversations as read.
|
||||||
|
* Method `POST`
|
||||||
|
* Authentication: required
|
||||||
|
* Params: None
|
||||||
|
* Response: JSON, returns a list of Mastodon Conversation entities that were marked as read (200 - healthy, 503 unhealthy).
|
||||||
|
|
||||||
## `GET /api/pleroma/emoji/packs`
|
## `GET /api/pleroma/emoji/packs`
|
||||||
### Lists the custom emoji packs on the server
|
### Lists the custom emoji packs on the server
|
||||||
* Method `GET`
|
* Method `GET`
|
||||||
|
|
|
@ -247,6 +247,35 @@ relates to mascots on the mastodon frontend
|
||||||
* `default_mascot`: An element from `mascots` - This will be used as the default mascot
|
* `default_mascot`: An element from `mascots` - This will be used as the default mascot
|
||||||
on MastoFE (default: `:pleroma_fox_tan`)
|
on MastoFE (default: `:pleroma_fox_tan`)
|
||||||
|
|
||||||
|
## :manifest
|
||||||
|
|
||||||
|
This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE.
|
||||||
|
|
||||||
|
* `icons`: Describe the icons of the app, this a list of maps describing icons in the same way as the
|
||||||
|
[spec](https://www.w3.org/TR/appmanifest/#imageresource-and-its-members) describes it.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
config :pleroma, :manifest,
|
||||||
|
icons: [
|
||||||
|
%{
|
||||||
|
src: "/static/logo.png"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
src: "/static/icon.png",
|
||||||
|
type: "image/png"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
src: "/static/icon.ico",
|
||||||
|
sizes: "72x72 96x96 128x128 256x256"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
* `theme_color`: Describe the theme color of the app. (Example: `"#282c37"`, `"rebeccapurple"`)
|
||||||
|
* `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`)
|
||||||
|
|
||||||
## :mrf_simple
|
## :mrf_simple
|
||||||
* `media_removal`: List of instances to remove medias from
|
* `media_removal`: List of instances to remove medias from
|
||||||
* `media_nsfw`: List of instances to put medias as NSFW(sensitive) from
|
* `media_nsfw`: List of instances to put medias as NSFW(sensitive) from
|
||||||
|
|
|
@ -91,7 +91,7 @@ sudo adduser -S -s /bin/false -h /opt/pleroma -H -G pleroma pleroma
|
||||||
```shell
|
```shell
|
||||||
sudo mkdir -p /opt/pleroma
|
sudo mkdir -p /opt/pleroma
|
||||||
sudo chown -R pleroma:pleroma /opt/pleroma
|
sudo chown -R pleroma:pleroma /opt/pleroma
|
||||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||||
```
|
```
|
||||||
|
|
||||||
* Change to the new directory:
|
* Change to the new directory:
|
||||||
|
|
|
@ -66,7 +66,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
|
||||||
```shell
|
```shell
|
||||||
sudo mkdir -p /opt/pleroma
|
sudo mkdir -p /opt/pleroma
|
||||||
sudo chown -R pleroma:pleroma /opt/pleroma
|
sudo chown -R pleroma:pleroma /opt/pleroma
|
||||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||||
```
|
```
|
||||||
|
|
||||||
* Change to the new directory:
|
* Change to the new directory:
|
||||||
|
|
|
@ -143,7 +143,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
|
||||||
```shell
|
```shell
|
||||||
sudo mkdir -p /opt/pleroma
|
sudo mkdir -p /opt/pleroma
|
||||||
sudo chown -R pleroma:pleroma /opt/pleroma
|
sudo chown -R pleroma:pleroma /opt/pleroma
|
||||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||||
```
|
```
|
||||||
|
|
||||||
* Change to the new directory:
|
* Change to the new directory:
|
||||||
|
|
|
@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
|
||||||
```shell
|
```shell
|
||||||
sudo mkdir -p /opt/pleroma
|
sudo mkdir -p /opt/pleroma
|
||||||
sudo chown -R pleroma:pleroma /opt/pleroma
|
sudo chown -R pleroma:pleroma /opt/pleroma
|
||||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||||
```
|
```
|
||||||
|
|
||||||
* Change to the new directory:
|
* Change to the new directory:
|
||||||
|
|
|
@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
|
||||||
```
|
```
|
||||||
sudo mkdir -p /opt/pleroma
|
sudo mkdir -p /opt/pleroma
|
||||||
sudo chown -R pleroma:pleroma /opt/pleroma
|
sudo chown -R pleroma:pleroma /opt/pleroma
|
||||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||||
```
|
```
|
||||||
|
|
||||||
* 新しいディレクトリに移動します。
|
* 新しいディレクトリに移動します。
|
||||||
|
|
|
@ -106,7 +106,7 @@ It is highly recommended you use your own fork for the `https://path/to/repo` pa
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pleroma$ cd ~
|
pleroma$ cd ~
|
||||||
pleroma$ git clone -b master https://path/to/repo
|
pleroma$ git clone -b stable https://path/to/repo
|
||||||
```
|
```
|
||||||
|
|
||||||
* Change to the new directory:
|
* Change to the new directory:
|
||||||
|
|
|
@ -96,9 +96,9 @@ rm -r ~pleroma/*
|
||||||
export FLAVOUR="arm64-musl"
|
export FLAVOUR="arm64-musl"
|
||||||
|
|
||||||
# Clone the release build into a temporary directory and unpack it
|
# Clone the release build into a temporary directory and unpack it
|
||||||
# Replace `master` with `develop` if you want to run the develop branch
|
# Replace `stable` with `unstable` if you want to run the unstable branch
|
||||||
su pleroma -s $SHELL -lc "
|
su pleroma -s $SHELL -lc "
|
||||||
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/master/download?job=$FLAVOUR' -o /tmp/pleroma.zip
|
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip
|
||||||
unzip /tmp/pleroma.zip -d /tmp/
|
unzip /tmp/pleroma.zip -d /tmp/
|
||||||
"
|
"
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ Clone the repository:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cd /home/pleroma
|
$ cd /home/pleroma
|
||||||
$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git
|
$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure Pleroma. Note that you need a domain name at this point:
|
Configure Pleroma. Note that you need a domain name at this point:
|
||||||
|
|
|
@ -29,7 +29,7 @@ This creates a "pleroma" login class and sets higher values than default for dat
|
||||||
Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): `useradd -m -L pleroma _pleroma`
|
Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): `useradd -m -L pleroma _pleroma`
|
||||||
|
|
||||||
#### Clone pleroma's directory
|
#### Clone pleroma's directory
|
||||||
Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b master https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide.
|
Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide.
|
||||||
|
|
||||||
#### Postgresql
|
#### Postgresql
|
||||||
Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql:
|
Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql:
|
||||||
|
|
|
@ -44,7 +44,7 @@ Vaihda pleroma-käyttäjään ja mene kotihakemistoosi:
|
||||||
|
|
||||||
Lataa pleroman lähdekoodi:
|
Lataa pleroman lähdekoodi:
|
||||||
|
|
||||||
`$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git`
|
`$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`
|
||||||
|
|
||||||
`$ cd pleroma`
|
`$ cd pleroma`
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ export FLAVOUR="arm64-musl"
|
||||||
|
|
||||||
# Clone the release build into a temporary directory and unpack it
|
# Clone the release build into a temporary directory and unpack it
|
||||||
su pleroma -s $SHELL -lc "
|
su pleroma -s $SHELL -lc "
|
||||||
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/master/download?job=$FLAVOUR' -o /tmp/pleroma.zip
|
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip
|
||||||
unzip /tmp/pleroma.zip -d /tmp/
|
unzip /tmp/pleroma.zip -d /tmp/
|
||||||
"
|
"
|
||||||
|
|
||||||
|
|
22
lib/mix/tasks/pleroma/count_statuses.ex
Normal file
22
lib/mix/tasks/pleroma/count_statuses.ex
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
defmodule Mix.Tasks.Pleroma.CountStatuses do
|
||||||
|
@shortdoc "Re-counts statuses for all users"
|
||||||
|
|
||||||
|
use Mix.Task
|
||||||
|
alias Pleroma.User
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
def run([]) do
|
||||||
|
Mix.Pleroma.start_pleroma()
|
||||||
|
|
||||||
|
stream =
|
||||||
|
User
|
||||||
|
|> where(local: true)
|
||||||
|
|> Pleroma.Repo.stream()
|
||||||
|
|
||||||
|
Pleroma.Repo.transaction(fn ->
|
||||||
|
Enum.each(stream, &User.update_note_count/1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
Mix.Pleroma.shell_info("Done")
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -111,19 +111,21 @@ def run(["get-packs" | args]) do
|
||||||
file_list: files_to_unzip
|
file_list: files_to_unzip
|
||||||
)
|
)
|
||||||
|
|
||||||
IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name]))
|
IO.puts(IO.ANSI.format(["Writing pack.json for ", :bright, pack_name]))
|
||||||
|
|
||||||
emoji_txt_str =
|
pack_json = %{
|
||||||
Enum.map(
|
pack: %{
|
||||||
files,
|
"license" => pack["license"],
|
||||||
fn {shortcode, path} ->
|
"homepage" => pack["homepage"],
|
||||||
emojo_path = Path.join("/emoji/#{pack_name}", path)
|
"description" => pack["description"],
|
||||||
"#{shortcode}, #{emojo_path}"
|
"fallback-src" => pack["src"],
|
||||||
end
|
"fallback-src-sha256" => pack["src_sha256"],
|
||||||
)
|
"share-files" => true
|
||||||
|> Enum.join("\n")
|
},
|
||||||
|
files: files
|
||||||
|
}
|
||||||
|
|
||||||
File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str)
|
File.write!(Path.join(pack_path, "pack.json"), Jason.encode!(pack_json, pretty: true))
|
||||||
else
|
else
|
||||||
IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
|
IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
|
||||||
end
|
end
|
||||||
|
|
|
@ -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,14 +35,10 @@ def run(["unfollow", target]) do
|
||||||
def run(["list"]) do
|
def run(["list"]) do
|
||||||
start_pleroma()
|
start_pleroma()
|
||||||
|
|
||||||
with %User{} = user <- Relay.get_actor() do
|
with {:ok, list} <- Relay.list() do
|
||||||
user
|
list |> Enum.each(&shell_info(&1))
|
||||||
|> User.following()
|
|
||||||
|> 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})
|
||||||
|
@ -63,6 +69,19 @@ def mark_as_read(participation) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mark_all_as_read(user) do
|
||||||
|
{_, participations} =
|
||||||
|
__MODULE__
|
||||||
|
|> where([p], p.user_id == ^user.id)
|
||||||
|
|> where([p], not p.read)
|
||||||
|
|> update([p], set: [read: true])
|
||||||
|
|> select([p], p)
|
||||||
|
|> Repo.update_all([])
|
||||||
|
|
||||||
|
User.set_unread_conversation_count(user)
|
||||||
|
{:ok, participations}
|
||||||
|
end
|
||||||
|
|
||||||
def mark_as_unread(participation) do
|
def mark_as_unread(participation) do
|
||||||
participation
|
participation
|
||||||
|> read_cng(%{read: false})
|
|> read_cng(%{read: false})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -17,6 +17,7 @@ defmodule Pleroma.Notification do
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
require Logger
|
||||||
|
|
||||||
@type t :: %__MODULE__{}
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
|
@ -34,43 +35,92 @@ def changeset(%Notification{} = notification, attrs) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def for_user_query(user, opts \\ []) do
|
def for_user_query(user, opts \\ []) do
|
||||||
query =
|
Notification
|
||||||
Notification
|
|> where(user_id: ^user.id)
|
||||||
|> where(user_id: ^user.id)
|
|> where(
|
||||||
|
[n, a],
|
||||||
|
fragment(
|
||||||
|
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
|
||||||
|
a.actor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> join(:inner, [n], activity in assoc(n, :activity))
|
||||||
|
|> join(:left, [n, a], object in Object,
|
||||||
|
on:
|
||||||
|
fragment(
|
||||||
|
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
|
||||||
|
object.data,
|
||||||
|
a.data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> preload([n, a, o], activity: {a, object: o})
|
||||||
|
|> exclude_muted(user, opts)
|
||||||
|
|> exclude_visibility(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp exclude_muted(query, _, %{with_muted: true}) do
|
||||||
|
query
|
||||||
|
end
|
||||||
|
|
||||||
|
defp exclude_muted(query, user, _opts) do
|
||||||
|
query
|
||||||
|
|> where([n, a], a.actor not in ^user.info.muted_notifications)
|
||||||
|
|> where([n, a], a.actor not in ^user.info.blocks)
|
||||||
|
|> where(
|
||||||
|
[n, a],
|
||||||
|
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
|
||||||
|
)
|
||||||
|
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
|
||||||
|
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
|
||||||
|
)
|
||||||
|
|> where([n, a, o, tm], is_nil(tm.user_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
@valid_visibilities ~w[direct unlisted public private]
|
||||||
|
|
||||||
|
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||||
|
when is_list(visibility) do
|
||||||
|
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
|
||||||
|
query
|
||||||
|> where(
|
|> where(
|
||||||
[n, a],
|
[n, a],
|
||||||
fragment(
|
not fragment(
|
||||||
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
|
"activity_visibility(?, ?, ?) = ANY (?)",
|
||||||
a.actor
|
a.actor,
|
||||||
|
a.recipients,
|
||||||
|
a.data,
|
||||||
|
^visibility
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> join(:inner, [n], activity in assoc(n, :activity))
|
|
||||||
|> join(:left, [n, a], object in Object,
|
|
||||||
on:
|
|
||||||
fragment(
|
|
||||||
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
|
|
||||||
object.data,
|
|
||||||
a.data
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|> preload([n, a, o], activity: {a, object: o})
|
|
||||||
|
|
||||||
if opts[:with_muted] do
|
|
||||||
query
|
|
||||||
else
|
else
|
||||||
where(query, [n, a], a.actor not in ^user.info.muted_notifications)
|
Logger.error("Could not exclude visibility to #{visibility}")
|
||||||
|> where([n, a], a.actor not in ^user.info.blocks)
|
query
|
||||||
|> where(
|
|
||||||
[n, a],
|
|
||||||
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
|
|
||||||
)
|
|
||||||
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
|
|
||||||
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
|
|
||||||
)
|
|
||||||
|> where([n, a, o, tm], is_nil(tm.user_id))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||||
|
when visibility in @valid_visibilities do
|
||||||
|
query
|
||||||
|
|> where(
|
||||||
|
[n, a],
|
||||||
|
not fragment(
|
||||||
|
"activity_visibility(?, ?, ?) = (?)",
|
||||||
|
a.actor,
|
||||||
|
a.recipients,
|
||||||
|
a.data,
|
||||||
|
^visibility
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||||
|
when visibility not in @valid_visibilities do
|
||||||
|
Logger.error("Could not exclude visibility to #{visibility}")
|
||||||
|
query
|
||||||
|
end
|
||||||
|
|
||||||
|
defp exclude_visibility(query, _visibility), do: query
|
||||||
|
|
||||||
def for_user(user, opts \\ %{}) do
|
def for_user(user, opts \\ %{}) do
|
||||||
user
|
user
|
||||||
|> for_user_query(opts)
|
|> for_user_query(opts)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -401,11 +401,9 @@ defp increase_read_duration(_) do
|
||||||
|
|
||||||
defp client, do: Pleroma.ReverseProxy.Client
|
defp client, do: Pleroma.ReverseProxy.Client
|
||||||
|
|
||||||
defp track_failed_url(url, code, opts) do
|
defp track_failed_url(url, error, opts) do
|
||||||
code = to_string(code)
|
|
||||||
|
|
||||||
ttl =
|
ttl =
|
||||||
if code in ["403", "404"] or String.starts_with?(code, "5") do
|
unless error in [:body_too_large, 400, 204] do
|
||||||
Keyword.get(opts, :failed_request_ttl, @failed_request_ttl)
|
Keyword.get(opts, :failed_request_ttl, @failed_request_ttl)
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -27,9 +27,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
|
||||||
|
@ -418,10 +416,6 @@ def follow(%User{} = follower, %User{info: info} = followed, state \\ "accept")
|
||||||
{: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
|
|
||||||
|
|
||||||
FollowingRelationship.follow(follower, followed, state)
|
FollowingRelationship.follow(follower, followed, state)
|
||||||
|
|
||||||
follower = maybe_update_following_count(follower)
|
follower = maybe_update_following_count(follower)
|
||||||
|
@ -576,12 +570,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
|
||||||
|
@ -678,7 +667,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
|
||||||
)
|
)
|
||||||
|
@ -699,7 +688,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
|
||||||
)
|
)
|
||||||
|
@ -769,7 +758,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
|
||||||
)
|
)
|
||||||
|
@ -1000,7 +989,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)
|
||||||
|
@ -1013,6 +1010,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
|
||||||
|
@ -1175,18 +1176,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)
|
||||||
|
@ -1241,11 +1231,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
|
||||||
|
@ -1566,6 +1551,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}
|
||||||
|
|
||||||
|
@ -269,22 +285,21 @@ def listen(%{to: to, actor: actor, context: context, object: object} = params) d
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def accept(%{to: to, actor: actor, object: object} = params) do
|
def accept(params) do
|
||||||
# only accept false as false value
|
accept_or_reject("Accept", params)
|
||||||
local = !(params[:local] == false)
|
|
||||||
|
|
||||||
with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object},
|
|
||||||
{:ok, activity} <- insert(data, local),
|
|
||||||
:ok <- maybe_federate(activity) do
|
|
||||||
{:ok, activity}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def reject(%{to: to, actor: actor, object: object} = params) do
|
def reject(params) do
|
||||||
# only accept false as false value
|
accept_or_reject("Reject", params)
|
||||||
local = !(params[:local] == false)
|
end
|
||||||
|
|
||||||
with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object},
|
def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
|
||||||
|
local = Map.get(params, :local, true)
|
||||||
|
activity_id = Map.get(params, :activity_id, nil)
|
||||||
|
|
||||||
|
with data <-
|
||||||
|
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|
||||||
|
|> Utils.maybe_put("id", activity_id),
|
||||||
{:ok, activity} <- insert(data, local),
|
{:ok, activity} <- insert(data, local),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
|
@ -409,18 +424,24 @@ def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
|
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options \\ []) do
|
||||||
|
local = Keyword.get(options, :local, true)
|
||||||
|
activity_id = Keyword.get(options, :activity_id, nil)
|
||||||
|
actor = Keyword.get(options, :actor, actor)
|
||||||
|
|
||||||
user = User.get_cached_by_ap_id(actor)
|
user = User.get_cached_by_ap_id(actor)
|
||||||
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
|
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
|
||||||
|
|
||||||
with {:ok, object, activity} <- Object.delete(object),
|
with {:ok, object, activity} <- Object.delete(object),
|
||||||
data <- %{
|
data <-
|
||||||
"type" => "Delete",
|
%{
|
||||||
"actor" => actor,
|
"type" => "Delete",
|
||||||
"object" => id,
|
"actor" => actor,
|
||||||
"to" => to,
|
"object" => id,
|
||||||
"deleted_activity_id" => activity && activity.id
|
"to" => to,
|
||||||
},
|
"deleted_activity_id" => activity && activity.id
|
||||||
|
}
|
||||||
|
|> maybe_put("id", activity_id),
|
||||||
{:ok, activity} <- insert(data, local, false),
|
{:ok, activity} <- insert(data, local, false),
|
||||||
stream_out_participations(object, user),
|
stream_out_participations(object, user),
|
||||||
_ <- decrease_replies_count_if_reply(object),
|
_ <- decrease_replies_count_if_reply(object),
|
||||||
|
@ -593,6 +614,49 @@ defp restrict_visibility(_query, %{visibility: visibility})
|
||||||
|
|
||||||
defp restrict_visibility(query, _visibility), do: query
|
defp restrict_visibility(query, _visibility), do: query
|
||||||
|
|
||||||
|
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||||
|
when is_list(visibility) do
|
||||||
|
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
|
||||||
|
from(
|
||||||
|
a in query,
|
||||||
|
where:
|
||||||
|
not fragment(
|
||||||
|
"activity_visibility(?, ?, ?) = ANY (?)",
|
||||||
|
a.actor,
|
||||||
|
a.recipients,
|
||||||
|
a.data,
|
||||||
|
^visibility
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Logger.error("Could not exclude visibility to #{visibility}")
|
||||||
|
query
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||||
|
when visibility in @valid_visibilities do
|
||||||
|
from(
|
||||||
|
a in query,
|
||||||
|
where:
|
||||||
|
not fragment(
|
||||||
|
"activity_visibility(?, ?, ?) = ?",
|
||||||
|
a.actor,
|
||||||
|
a.recipients,
|
||||||
|
a.data,
|
||||||
|
^visibility
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||||
|
when visibility not in @valid_visibilities do
|
||||||
|
Logger.error("Could not exclude visibility to #{visibility}")
|
||||||
|
query
|
||||||
|
end
|
||||||
|
|
||||||
|
defp exclude_visibility(query, _visibility), do: query
|
||||||
|
|
||||||
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
|
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
|
||||||
do: query
|
do: query
|
||||||
|
|
||||||
|
@ -957,6 +1021,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
||||||
|> restrict_muted_reblogs(opts)
|
|> restrict_muted_reblogs(opts)
|
||||||
|> Activity.restrict_deactivated_users()
|
|> Activity.restrict_deactivated_users()
|
||||||
|> exclude_poll_votes(opts)
|
|> exclude_poll_votes(opts)
|
||||||
|
|> exclude_visibility(opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
|
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
|
||||||
|
@ -1156,7 +1221,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
|
||||||
|
|
|
@ -518,7 +518,7 @@ def handle_incoming(
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
|
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
|
||||||
_options
|
_options
|
||||||
) do
|
) do
|
||||||
with actor <- Containment.get_actor(data),
|
with actor <- Containment.get_actor(data),
|
||||||
|
@ -532,7 +532,8 @@ def handle_incoming(
|
||||||
type: "Accept",
|
type: "Accept",
|
||||||
actor: followed,
|
actor: followed,
|
||||||
object: follow_activity.data["id"],
|
object: follow_activity.data["id"],
|
||||||
local: false
|
local: false,
|
||||||
|
activity_id: id
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
_e -> :error
|
_e -> :error
|
||||||
|
@ -540,7 +541,7 @@ def handle_incoming(
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
|
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
|
||||||
_options
|
_options
|
||||||
) do
|
) do
|
||||||
with actor <- Containment.get_actor(data),
|
with actor <- Containment.get_actor(data),
|
||||||
|
@ -555,7 +556,8 @@ def handle_incoming(
|
||||||
type: "Reject",
|
type: "Reject",
|
||||||
actor: followed,
|
actor: followed,
|
||||||
object: follow_activity.data["id"],
|
object: follow_activity.data["id"],
|
||||||
local: false
|
local: false,
|
||||||
|
activity_id: id
|
||||||
}) do
|
}) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
|
@ -640,7 +642,7 @@ def handle_incoming(
|
||||||
# an error or a tombstone. This would allow us to verify that a deletion actually took
|
# an error or a tombstone. This would allow us to verify that a deletion actually took
|
||||||
# place.
|
# place.
|
||||||
def handle_incoming(
|
def handle_incoming(
|
||||||
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
|
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
|
||||||
_options
|
_options
|
||||||
) do
|
) do
|
||||||
object_id = Utils.get_ap_id(object_id)
|
object_id = Utils.get_ap_id(object_id)
|
||||||
|
@ -649,7 +651,8 @@ def handle_incoming(
|
||||||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
||||||
{:ok, object} <- get_obj_helper(object_id),
|
{:ok, object} <- get_obj_helper(object_id),
|
||||||
:ok <- Containment.contain_origin(actor.ap_id, object.data),
|
:ok <- Containment.contain_origin(actor.ap_id, object.data),
|
||||||
{:ok, activity} <- ActivityPub.delete(object, false) do
|
{:ok, activity} <-
|
||||||
|
ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
else
|
else
|
||||||
nil ->
|
nil ->
|
||||||
|
@ -1053,7 +1056,6 @@ defp strip_internal_tags(object), do: object
|
||||||
def perform(:user_upgrade, user) do
|
def perform(:user_upgrade, user) do
|
||||||
# we pass a fake user so that the followers collection is stripped away
|
# we pass a fake user so that the followers collection is stripped away
|
||||||
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
|
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
|
||||||
maybe_retire_websub(user.ap_id)
|
|
||||||
|
|
||||||
from(
|
from(
|
||||||
a in Activity,
|
a in Activity,
|
||||||
|
@ -1095,19 +1097,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
|
||||||
|
|
|
@ -34,6 +34,12 @@ def index(%{assigns: %{user: user}} = conn, _params) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "GET /web/manifest.json"
|
||||||
|
def manifest(conn, _params) do
|
||||||
|
conn
|
||||||
|
|> render("manifest.json")
|
||||||
|
end
|
||||||
|
|
||||||
@doc "PUT /api/web/settings"
|
@doc "PUT /api/web/settings"
|
||||||
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
|
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
|
||||||
with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
|
with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
|
||||||
|
|
|
@ -167,7 +167,11 @@ def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
|
||||||
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
||||||
true <- Visibility.visible_for_user?(activity, user) do
|
true <- Visibility.visible_for_user?(activity, user) do
|
||||||
try_render(conn, "show.json", activity: activity, for: user)
|
try_render(conn, "show.json",
|
||||||
|
activity: activity,
|
||||||
|
for: user,
|
||||||
|
with_direct_conversation_id: true
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,7 @@ def get_scheduled_activities(user, params \\ %{}) do
|
||||||
defp cast_params(params) do
|
defp cast_params(params) do
|
||||||
param_types = %{
|
param_types = %{
|
||||||
exclude_types: {:array, :string},
|
exclude_types: {:array, :string},
|
||||||
|
exclude_visibilities: {:array, :string},
|
||||||
reblogs: :boolean,
|
reblogs: :boolean,
|
||||||
with_muted: :boolean
|
with_muted: :boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, 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
|
|
|
@ -79,6 +79,15 @@ def update_conversation(
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def read_conversations(%{assigns: %{user: user}} = conn, _params) do
|
||||||
|
with {:ok, participations} <- Participation.mark_all_as_read(user) do
|
||||||
|
conn
|
||||||
|
|> add_link_headers(participations)
|
||||||
|
|> put_view(ConversationView)
|
||||||
|
|> render("participations.json", participations: participations, for: user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
|
def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
|
||||||
with {:ok, notification} <- Notification.read_one(user, notification_id) do
|
with {:ok, notification} <- Notification.read_one(user, notification_id) do
|
||||||
conn
|
conn
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
@ -256,6 +266,7 @@ defmodule Pleroma.Web.Router do
|
||||||
|
|
||||||
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
|
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
|
||||||
get("/conversations/:id", PleromaAPIController, :conversation)
|
get("/conversations/:id", PleromaAPIController, :conversation)
|
||||||
|
post("/conversations/read", PleromaAPIController, :read_conversations)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope [] do
|
scope [] do
|
||||||
|
@ -499,11 +510,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
|
||||||
|
|
||||||
|
@ -586,6 +592,12 @@ defmodule Pleroma.Web.Router do
|
||||||
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
|
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope "/", Pleroma.Web do
|
||||||
|
pipe_through(:api)
|
||||||
|
|
||||||
|
get("/web/manifest.json", MastoFEController, :manifest)
|
||||||
|
end
|
||||||
|
|
||||||
scope "/", Pleroma.Web do
|
scope "/", Pleroma.Web do
|
||||||
pipe_through(:mastodon_html)
|
pipe_through(:mastodon_html)
|
||||||
|
|
||||||
|
|
|
@ -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 %>
|
||||||
|
|
|
@ -4,9 +4,13 @@
|
||||||
<meta charset='utf-8'>
|
<meta charset='utf-8'>
|
||||||
<meta content='width=device-width, initial-scale=1' name='viewport'>
|
<meta content='width=device-width, initial-scale=1' name='viewport'>
|
||||||
<title>
|
<title>
|
||||||
<%= Pleroma.Config.get([:instance, :name]) %>
|
<%= Config.get([:instance, :name]) %>
|
||||||
</title>
|
</title>
|
||||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||||
|
<link rel="manifest" type="applicaton/manifest+json" href="<%= masto_fe_path(Pleroma.Web.Endpoint, :manifest) %>" />
|
||||||
|
|
||||||
|
<meta name="theme-color" content="<%= Config.get([:manifest, :theme_color]) %>" />
|
||||||
|
|
||||||
<script crossorigin='anonymous' src="/packs/locales.js"></script>
|
<script crossorigin='anonymous' src="/packs/locales.js"></script>
|
||||||
<script crossorigin='anonymous' src="/packs/locales/glitch/en.js"></script>
|
<script crossorigin='anonymous' src="/packs/locales/glitch/en.js"></script>
|
||||||
|
|
||||||
|
|
|
@ -99,4 +99,23 @@ def initial_state(token, user, custom_emojis) do
|
||||||
defp present?(nil), do: false
|
defp present?(nil), do: false
|
||||||
defp present?(false), do: false
|
defp present?(false), do: false
|
||||||
defp present?(_), do: true
|
defp present?(_), do: true
|
||||||
|
|
||||||
|
def render("manifest.json", _params) do
|
||||||
|
%{
|
||||||
|
name: Config.get([:instance, :name]),
|
||||||
|
description: Config.get([:instance, :description]),
|
||||||
|
icons: Config.get([:manifest, :icons]),
|
||||||
|
theme_color: Config.get([:manifest, :theme_color]),
|
||||||
|
background_color: Config.get([:manifest, :background_color]),
|
||||||
|
display: "standalone",
|
||||||
|
scope: Pleroma.Web.base_url(),
|
||||||
|
start_url: masto_fe_path(Pleroma.Web.Endpoint, :index, ["getting-started"]),
|
||||||
|
categories: [
|
||||||
|
"social"
|
||||||
|
],
|
||||||
|
serviceworker: %{
|
||||||
|
src: "/sw.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
8
mix.exs
8
mix.exs
|
@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :pleroma,
|
app: :pleroma,
|
||||||
version: version("1.0.0"),
|
version: version("1.1.50"),
|
||||||
elixir: "~> 1.8",
|
elixir: "~> 1.8",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
||||||
|
@ -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"]
|
||||||
|
|
||||||
|
@ -220,7 +221,10 @@ defp version(version) do
|
||||||
with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
|
with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
|
||||||
branch_name <- String.trim(branch_name),
|
branch_name <- String.trim(branch_name),
|
||||||
branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
|
branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
|
||||||
true <- branch_name not in ["master", "HEAD"] do
|
true <-
|
||||||
|
!Enum.any?(["master", "HEAD", "release/", "stable"], fn name ->
|
||||||
|
String.starts_with?(name, branch_name)
|
||||||
|
end) do
|
||||||
branch_name =
|
branch_name =
|
||||||
branch_name
|
branch_name
|
||||||
|> String.trim()
|
|> String.trim()
|
||||||
|
|
|
@ -3,14 +3,13 @@ defmodule Pleroma.Repo.Migrations.CreatePleroma.User do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create_if_not_exists table(:users) do
|
create_if_not_exists table(:users) do
|
||||||
add :email, :string
|
add(:email, :string)
|
||||||
add :password_hash, :string
|
add(:password_hash, :string)
|
||||||
add :name, :string
|
add(:name, :string)
|
||||||
add :nickname, :string
|
add(:nickname, :string)
|
||||||
add :bio, :string
|
add(:bio, :string)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,12 +3,11 @@ defmodule Pleroma.Repo.Migrations.CreatePleroma.Activity do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create_if_not_exists table(:activities) do
|
create_if_not_exists table(:activities) do
|
||||||
add :data, :map
|
add(:data, :map)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
create_if_not_exists index(:activities, [:data], using: :gin)
|
create_if_not_exists(index(:activities, [:data], using: :gin))
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,10 +3,9 @@ defmodule Pleroma.Repo.Migrations.CreatePleroma.Object do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create_if_not_exists table(:objects) do
|
create_if_not_exists table(:objects) do
|
||||||
add :data, :map
|
add(:data, :map)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule Pleroma.Repo.Migrations.AddFollowingListToUsers do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
add :following, :map
|
add(:following, :map)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule Pleroma.Repo.Migrations.AddApIdToUsers do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
add :ap_id, :string
|
add(:ap_id, :string)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,6 @@ defmodule Pleroma.Repo.Migrations.AddIndexToObjects do
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create_if_not_exists index(:objects, [:data], using: :gin)
|
create_if_not_exists(index(:objects, [:data], using: :gin))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@ defmodule Pleroma.Repo.Migrations.AddUniqueIndexToEmailAndNickname do
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create_if_not_exists unique_index(:users, [:email])
|
create_if_not_exists(unique_index(:users, [:email]))
|
||||||
create_if_not_exists unique_index(:users, [:nickname])
|
create_if_not_exists(unique_index(:users, [:nickname]))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule Pleroma.Repo.Migrations.AddAvatarObjectToUsers do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
add :avatar, :map
|
add(:avatar, :map)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,11 @@ defmodule Pleroma.Repo.Migrations.CreateWebsubServerSubscription do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create_if_not_exists table(:websub_server_subscriptions) do
|
create_if_not_exists table(:websub_server_subscriptions) do
|
||||||
add :topic, :string
|
add(:topic, :string)
|
||||||
add :callback, :string
|
add(:callback, :string)
|
||||||
add :secret, :string
|
add(:secret, :string)
|
||||||
add :valid_until, :naive_datetime
|
add(:valid_until, :naive_datetime)
|
||||||
add :state, :string
|
add(:state, :string)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,8 @@ defmodule Pleroma.Repo.Migrations.AddFieldsToUsers do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
add :local, :boolean, default: true
|
add(:local, :boolean, default: true)
|
||||||
add :info, :map
|
add(:info, :map)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,11 @@ defmodule Pleroma.Repo.Migrations.CreateWebsubClientSubscription do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create_if_not_exists table(:websub_client_subscriptions) do
|
create_if_not_exists table(:websub_client_subscriptions) do
|
||||||
add :topic, :string
|
add(:topic, :string)
|
||||||
add :secret, :string
|
add(:secret, :string)
|
||||||
add :valid_until, :naive_datetime_usec
|
add(:valid_until, :naive_datetime_usec)
|
||||||
add :state, :string
|
add(:state, :string)
|
||||||
add :subscribers, :map
|
add(:subscribers, :map)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,8 @@ defmodule Pleroma.Repo.Migrations.AddUserAndHub do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
alter table(:websub_client_subscriptions) do
|
alter table(:websub_client_subscriptions) do
|
||||||
add :hub, :string
|
add(:hub, :string)
|
||||||
add :user_id, references(:users)
|
add(:user_id, references(:users))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,10 +2,16 @@ defmodule Pleroma.Repo.Migrations.AddIdContraintsToActivitiesAndObjectsPartTwo d
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
def up do
|
def up do
|
||||||
drop_if_exists index(:objects, ["(data->>\"id\")"], name: :objects_unique_apid_index)
|
drop_if_exists(index(:objects, ["(data->>\"id\")"], name: :objects_unique_apid_index))
|
||||||
drop_if_exists index(:activities, ["(data->>\"id\")"], name: :activities_unique_apid_index)
|
drop_if_exists(index(:activities, ["(data->>\"id\")"], name: :activities_unique_apid_index))
|
||||||
create_if_not_exists unique_index(:objects, ["(data->>'id')"], name: :objects_unique_apid_index)
|
|
||||||
create_if_not_exists unique_index(:activities, ["(data->>'id')"], name: :activities_unique_apid_index)
|
create_if_not_exists(
|
||||||
|
unique_index(:objects, ["(data->>'id')"], name: :objects_unique_apid_index)
|
||||||
|
)
|
||||||
|
|
||||||
|
create_if_not_exists(
|
||||||
|
unique_index(:activities, ["(data->>'id')"], name: :activities_unique_apid_index)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def down, do: :ok
|
def down, do: :ok
|
||||||
|
|
|
@ -3,9 +3,9 @@ defmodule Pleroma.Repo.Migrations.AddLocalFieldToActivities do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
alter table(:activities) do
|
alter table(:activities) do
|
||||||
add :local, :boolean, default: true
|
add(:local, :boolean, default: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
create_if_not_exists index(:activities, [:local])
|
create_if_not_exists(index(:activities, [:local]))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,6 @@ defmodule Pleroma.Repo.Migrations.AddUniqueIndexToAPID do
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create_if_not_exists unique_index(:users, [:ap_id])
|
create_if_not_exists(unique_index(:users, [:ap_id]))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,14 +3,13 @@ defmodule Pleroma.Repo.Migrations.LongerBios do
|
||||||
|
|
||||||
def up do
|
def up do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
modify :bio, :text
|
modify(:bio, :text)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def down do
|
def down do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
modify :bio, :string
|
modify(:bio, :string)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,6 @@ defmodule Pleroma.Repo.Migrations.RemoveActivitiesIndex do
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
drop_if_exists index(:activities, [:data])
|
drop_if_exists(index(:activities, [:data]))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,16 @@ defmodule Pleroma.Repo.Migrations.AddObjectActivityIndexPartTwo do
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
drop_if_exists index(:objects, ["(data->'object'->>'id')", "(data->>'type')"], name: :activities_create_objects_index)
|
drop_if_exists(
|
||||||
create_if_not_exists index(:activities, ["(data->'object'->>'id')", "(data->>'type')"], name: :activities_create_objects_index)
|
index(:objects, ["(data->'object'->>'id')", "(data->>'type')"],
|
||||||
|
name: :activities_create_objects_index
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
create_if_not_exists(
|
||||||
|
index(:activities, ["(data->'object'->>'id')", "(data->>'type')"],
|
||||||
|
name: :activities_create_objects_index
|
||||||
|
)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,8 @@ defmodule Pleroma.Repo.Migrations.AddActorIndexToActivity do
|
||||||
use Ecto.Migration
|
use Ecto.Migration
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create_if_not_exists index(:activities, ["(data->>'actor')", "inserted_at desc"], name: :activities_actor_index)
|
create_if_not_exists(
|
||||||
|
index(:activities, ["(data->>'actor')", "inserted_at desc"], name: :activities_actor_index)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,13 +3,13 @@ defmodule Pleroma.Repo.Migrations.AddFollowerAddressToUser do
|
||||||
|
|
||||||
def up do
|
def up do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
add :follower_address, :string, unique: true
|
add(:follower_address, :string, unique: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def down do
|
def down do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
remove :follower_address
|
remove(:follower_address)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,12 +3,12 @@ defmodule Pleroma.Repo.Migrations.AddMastodonApps do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create_if_not_exists table(:apps) do
|
create_if_not_exists table(:apps) do
|
||||||
add :client_name, :string
|
add(:client_name, :string)
|
||||||
add :redirect_uris, :string
|
add(:redirect_uris, :string)
|
||||||
add :scopes, :string
|
add(:scopes, :string)
|
||||||
add :website, :string
|
add(:website, :string)
|
||||||
add :client_id, :string
|
add(:client_id, :string)
|
||||||
add :client_secret, :string
|
add(:client_secret, :string)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,11 @@ defmodule Pleroma.Repo.Migrations.CreateOAuthAuthorizations do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
create_if_not_exists table(:oauth_authorizations) do
|
create_if_not_exists table(:oauth_authorizations) do
|
||||||
add :app_id, references(:apps)
|
add(:app_id, references(:apps))
|
||||||
add :user_id, references(:users)
|
add(:user_id, references(:users))
|
||||||
add :token, :string
|
add(:token, :string)
|
||||||
add :valid_until, :naive_datetime_usec
|
add(:valid_until, :naive_datetime_usec)
|
||||||
add :used, :boolean, default: false
|
add(:used, :boolean, default: false)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue