Page MenuHomePhorge

No OneTemporary

Size
1 MB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 36137b38e..04af8c186 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,226 +1,257 @@
image: elixir:1.8.1
variables:
POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DB_HOST: postgres
MIX_ENV: test
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- deps
- _build
stages:
- build
- test
+ - benchmark
- deploy
- release
before_script:
- mix local.hex --force
- mix local.rebar --force
build:
stage: build
script:
- mix deps.get
- mix compile --force
+docs-build:
+ stage: build
+ only:
+ - master@pleroma/pleroma
+ - develop@pleroma/pleroma
+ variables:
+ MIX_ENV: dev
+ PLEROMA_BUILD_ENV: prod
+ script:
+ - mix deps.get
+ - mix compile
+ - mix docs
+ artifacts:
+ paths:
+ - priv/static/doc
+
+benchmark:
+ stage: benchmark
+ variables:
+ MIX_ENV: benchmark
+ services:
+ - name: lainsoykaf/postgres-with-rum
+ alias: postgres
+ command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
+ script:
+ - mix deps.get
+ - mix ecto.create
+ - mix ecto.migrate
+ - mix pleroma.load_testing
+
unit-testing:
stage: test
services:
- 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 coveralls --trace --preload-modules
unit-testing-rum:
stage: test
services:
- name: lainsoykaf/postgres-with-rum
alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
variables:
RUM_ENABLED: "true"
script:
- mix deps.get
- mix ecto.create
- mix ecto.migrate
- "mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/"
- mix test --trace --preload-modules
lint:
stage: test
script:
- mix format --check-formatted
analysis:
stage: test
script:
- mix deps.get
- mix credo --strict --only=warnings,todo,fixme,consistency,readability
docs-deploy:
stage: deploy
image: alpine:latest
only:
- stable@pleroma/pleroma
- develop@pleroma/pleroma
before_script:
- apk add curl
script:
- curl -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline
review_app:
image: alpine:3.9
stage: deploy
before_script:
- apk update && apk add openssh-client git
when: manual
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.pleroma.online/
on_stop: stop_review_app
only:
- branches
except:
- master
- develop
script:
- echo "$CI_ENVIRONMENT_SLUG"
- mkdir -p ~/.ssh
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts
- (ssh -t dokku@pleroma.online -- apps:create "$CI_ENVIRONMENT_SLUG") || true
- ssh -t dokku@pleroma.online -- config:set "$CI_ENVIRONMENT_SLUG" APP_NAME="$CI_ENVIRONMENT_SLUG" APP_HOST="$CI_ENVIRONMENT_SLUG.pleroma.online" MIX_ENV=dokku
- (ssh -t dokku@pleroma.online -- postgres:create $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db) || true
- (ssh -t dokku@pleroma.online -- postgres:link $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db "$CI_ENVIRONMENT_SLUG") || true
- (ssh -t dokku@pleroma.online -- certs:add "$CI_ENVIRONMENT_SLUG" /home/dokku/server.crt /home/dokku/server.key) || true
- git push -f dokku@pleroma.online:$CI_ENVIRONMENT_SLUG $CI_COMMIT_SHA:refs/heads/master
stop_review_app:
image: alpine:3.9
stage: deploy
before_script:
- apk update && apk add openssh-client git
when: manual
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
script:
- echo "$CI_ENVIRONMENT_SLUG"
- mkdir -p ~/.ssh
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts
- ssh -t dokku@pleroma.online -- --force apps:destroy "$CI_ENVIRONMENT_SLUG"
- ssh -t dokku@pleroma.online -- --force postgres:destroy $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db
amd64:
stage: release
# TODO: Replace with upstream image when 1.9.0 comes out
image: rinpatch/elixir:1.9.0-rc.0
only: &release-only
- stable@pleroma/pleroma
- develop@pleroma/pleroma
- /^maint/.*$/@pleroma/pleroma
- /^release/.*$/@pleroma/pleroma
artifacts: &release-artifacts
name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME"
paths:
- release/*
# Ideally it would be never for master branch and with the next commit for develop,
# but Gitlab does not support neither `only` for artifacts
# nor setting it to never from .gitlab-ci.yml
# nor expiring with the next commit
expire_in: 42 yrs
cache: &release-cache
key: $CI_COMMIT_REF_NAME-$CI_JOB_NAME
paths:
- deps
variables: &release-variables
MIX_ENV: prod
before_script: &before-release
- echo "import Mix.Config" > config/prod.secret.exs
- mix local.hex --force
- mix local.rebar --force
script: &release
- mix deps.get --only prod
- mkdir release
- export PLEROMA_BUILD_BRANCH=$CI_COMMIT_REF_NAME
- mix release --path release
amd64-musl:
stage: release
artifacts: *release-artifacts
only: *release-only
# TODO: Replace with upstream image when 1.9.0 comes out
image: rinpatch/elixir:1.9.0-rc.0-alpine
cache: *release-cache
variables: *release-variables
before_script: &before-release-musl
- apk add git gcc g++ musl-dev make
- echo "import Mix.Config" > config/prod.secret.exs
- mix local.hex --force
- mix local.rebar --force
script: *release
arm:
stage: release
artifacts: *release-artifacts
only: *release-only
tags:
- arm32
# TODO: Replace with upstream image when 1.9.0 comes out
image: rinpatch/elixir:1.9.0-rc.0-arm
cache: *release-cache
variables: *release-variables
before_script: *before-release
script: *release
arm-musl:
stage: release
artifacts: *release-artifacts
only: *release-only
tags:
- arm32
# TODO: Replace with upstream image when 1.9.0 comes out
image: rinpatch/elixir:1.9.0-rc.0-arm-alpine
cache: *release-cache
variables: *release-variables
before_script: *before-release-musl
script: *release
arm64:
stage: release
artifacts: *release-artifacts
only: *release-only
tags:
- arm
# TODO: Replace with upstream image when 1.9.0 comes out
image: rinpatch/elixir:1.9.0-rc.0-arm64
cache: *release-cache
variables: *release-variables
before_script: *before-release
script: *release
arm64-musl:
stage: release
artifacts: *release-artifacts
only: *release-only
tags:
- arm
# TODO: Replace with upstream image when 1.9.0 comes out
image: rinpatch/elixir:1.9.0-rc.0-arm64-alpine
cache: *release-cache
variables: *release-variables
before_script: *before-release-musl
script: *release
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4744567d1..00dd62e24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,385 +1,446 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
+### Removed
+- **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`
+- **Breaking**: OStatus protocol support
+
+### Changed
+- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
+- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
+- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
+- Enabled `:instance, extended_nickname_format` in the default config
+- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
+- Extract RSS functionality from OStatus
+- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities
+<details>
+ <summary>API Changes</summary>
+
+- **Breaking:** Admin API: Return link alongside with token on password reset
+- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
+- Admin API: Return `total` when querying for reports
+- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
+- Admin API: Return link alongside with token on password reset
+- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`)
+- Mastodon API: `pleroma.thread_muted` to the Status entity
+- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
+</details>
+
### Added
- Refreshing poll results for remote polls
+- Authentication: Added rate limit for password-authorized actions / login existence checks
+- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
+- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
+<details>
+ <summary>API Changes</summary>
+
- Job queue stats to the healthcheck page
- Admin API: Add ability to require password reset
- Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition)
- Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items
- Pleroma API: `POST /api/v1/pleroma/scrobble` to scrobble a media item
- Mastodon API: Add `upload_limit`, `avatar_upload_limit`, `background_upload_limit`, and `banner_upload_limit` to `/api/v1/instance`
- Mastodon API: Add `pleroma.unread_conversation_count` to the Account entity
- OAuth: support for hierarchical permissions / [Mastodon 2.4.3 OAuth permissions](https://docs.joinmastodon.org/api/permissions/)
-- Authentication: Added rate limit for password-authorized actions / login existence checks
- Metadata Link: Atom syndication Feed
-- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- Mastodon API: Add `exclude_visibilities` parameter to the timeline and notification endpoints
+- Admin API: `/users/:nickname/toggle_activation` endpoint is now deprecated in favor of: `/users/activate`, `/users/deactivate`, both accept `nicknames` array
+- Admin API: `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group` (both accept `nicknames` array), `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body).
+- Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays
+- Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read
+- Mastodon API: Add `/api/v1/markers` for managing timeline read markers
### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
- **Breaking:** Admin API: Return link alongside with token on password reset
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
- Admin API: Return link alongside with token on password reset
- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities
- OStatus: Extract RSS functionality
- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`)
+- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
+- Deprecated `User.Info` embedded schema (fields moved to `User`)
- Store status data inside Flag activity
+</details>
### Fixed
+- Report emails now include functional links to profiles of remote user accounts
+<details>
+ <summary>API Changes</summary>
+
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
-- Added `:instance, extended_nickname_format` setting to the default config
-- Report emails now include functional links to profiles of remote user accounts
+</details>
+
+## [1.1.2] - 2019-10-18
+### Fixed
+- `pleroma_ctl` trying to connect to a running instance when generating the config, which of course doesn't exist.
-## [1.1.0] - 2019-??-??
-**Breaking:** The stable branch has been changed from `master` to `stable`, `master` now points to `release/1.0`
+## [1.1.1] - 2019-10-18
+### Fixed
+- One of the migrations between 1.0.0 and 1.1.0 wiping user info of the relay user because of unexpected behavior of postgresql's `jsonb_set`, resulting in inability to post in the default configuration. If you were affected, please run the following query in postgres console, the relay user will be recreated automatically:
+```
+delete from users where ap_id = 'https://your.instance.hostname/relay';
+```
+- Bad user search matches
+
+## [1.1.0] - 2019-10-14
+**Breaking:** The stable branch has been changed from `master` to `stable`. If you want to keep using 1.0, the `release/1.0` branch will receive security updates for 6 months after 1.1 release.
+
+**OTP Note:** `pleroma_ctl` in 1.0 defaults to `master` and doesn't support specifying arbitrary branches, making `./pleroma_ctl update` fail. To fix this, fetch a version of `pleroma_ctl` from 1.1 using the command below and proceed with the update normally:
+```
+curl -Lo ./bin/pleroma_ctl 'https://git.pleroma.social/pleroma/pleroma/raw/develop/rel/files/bin/pleroma_ctl'
+```
### Security
- Mastodon API: respect post privacy in `/api/v1/statuses/:id/{favourited,reblogged}_by`
### Removed
- **Breaking:** GNU Social API with Qvitter extensions support
- Emoji: Remove longfox emojis.
- Remove `Reply-To` header from report emails for admins.
+- ActivityPub: The `/objects/:uuid/likes` endpoint.
### Changed
- **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config
- **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired
- **Breaking:** `/api/pleroma/notifications/read` is moved to `/api/v1/pleroma/notifications/read` and now supports `max_id` and responds with Mastodon API entities.
-- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
- Configuration: added `config/description.exs`, from which `docs/config.md` is generated
- Configuration: OpenGraph and TwitterCard providers enabled by default
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
-- Mastodon API: `pleroma.thread_muted` key in the Status entity
- Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set
- NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
- NodeInfo: Return `mailerEnabled` in `metadata`
- Mastodon API: Unsubscribe followers when they unfollow a user
- Mastodon API: `pleroma.thread_muted` key in the Status entity
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- Improve digest email template
– Pagination: (optional) return `total` alongside with `items` when paginating
-- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
+- The `Pleroma.FlakeId` module has been replaced with the `flake_id` library.
### Fixed
- Following from Osada
- Favorites timeline doing database-intensive queries
- Metadata rendering errors resulting in the entire page being inaccessible
- `federation_incoming_replies_max_depth` option being ignored in certain cases
- Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`)
- Mastodon API: Misskey's endless polls being unable to render
- Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity
- Mastodon API: Notifications endpoint crashing if one notification failed to render
+- Mastodon API: `exclude_replies` is correctly handled again.
- Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`)
- Mastodon API, streaming: Fix filtering of notifications based on blocks/mutes/thread mutes
-- ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set
-- Existing user id not being preserved on insert conflict
+- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
+- Mastodon API: Ensure the `account` field is not empty when rendering Notification entities.
+- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
+- Mastodon API: Blocks are now treated consistently between the Streaming API and the Timeline APIs
- Rich Media: Parser failing when no TTL can be found by image TTL setters
- Rich Media: The crawled URL is now spliced into the rich media data.
- ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification.
-- Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected.
-- Report email not being sent to admins when the reporter is a remote user
-- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances
+- ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set
- ActivityPub: Deactivated user deletion
- ActivityPub: Fix `/users/:nickname/inbox` crashing without an authenticated user
- MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled
-- Mastodon API: Blocks are now treated consistently between the Streaming API and the Timeline APIs
-- Mastodon API: `exclude_replies` is correctly handled again.
+- ActivityPub: Correct addressing of Undo.
+- ActivityPub: Correct addressing of profile update activities.
+- ActivityPub: Polls are now refreshed when necessary.
+- Report emails now include functional links to profiles of remote user accounts
+- Existing user id not being preserved on insert conflict
+- Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected.
+- Report email not being sent to admins when the reporter is a remote user
+- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances
### Added
- Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically.
- Mastodon API: in post_status, the expires_in parameter lets you set the number of seconds until an activity expires. It must be at least one hour.
- Mastodon API: all status JSON responses contain a `pleroma.expires_at` item which states when an activity will expire. The value is only shown to the user who created the activity. To everyone else it's empty.
- Configuration: `ActivityExpiration.enabled` controls whether expired activites will get deleted at the appropriate time. Enabled by default.
- Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data.
- MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
- MRF: Support for excluding specific domains from Transparency.
- MRF: Support for filtering posts based on who they mention (`Pleroma.Web.ActivityPub.MRF.MentionPolicy`)
- Mastodon API: Support for the [`tagged` filter](https://github.com/tootsuite/mastodon/pull/9755) in [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/api/rest/accounts/#get-api-v1-accounts-id-statuses)
- Mastodon API, streaming: Add support for passing the token in the `Sec-WebSocket-Protocol` header
- Mastodon API, extension: Ability to reset avatar, profile banner, and background
+- Mastodon API: Add support for `fields_attributes` API parameter (setting custom fields)
- Mastodon API: Add support for categories for custom emojis by reusing the group feature. <https://github.com/tootsuite/mastodon/pull/11196>
- Mastodon API: Add support for muting/unmuting notifications
- Mastodon API: Add support for the `blocked_by` attribute in the relationship API (`GET /api/v1/accounts/relationships`). <https://github.com/tootsuite/mastodon/pull/10373>
- Mastodon API: Add support for the `domain_blocking` attribute in the relationship API (`GET /api/v1/accounts/relationships`).
- Mastodon API: Add `pleroma.deactivated` to the Account entity
- Mastodon API: added `/auth/password` endpoint for password reset with rate limit.
- Mastodon API: /api/v1/accounts/:id/statuses now supports nicknames or user id
- Mastodon API: Improve support for the user profile custom fields
-- Mastodon API: follower/following counters are nullified when `hide_follows`/`hide_followers` and `hide_follows_count`/`hide_followers_count` are set
+- Mastodon API: Add support for `fields_attributes` API parameter (setting custom fields)
+- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
- Admin API: Return users' tags when querying reports
- Admin API: Return avatar and display name when querying users
- Admin API: Allow querying user by ID
- Admin API: Added support for `tuples`.
- Admin API: Added endpoints to run mix tasks pleroma.config migrate_to_db & pleroma.config migrate_from_db
- Added synchronization of following/followers counters for external users
- Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`.
- Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options.
- Configuration: `user_bio_length` and `user_name_length` options.
- Addressable lists
- Twitter API: added rate limit for `/api/account/password_reset` endpoint.
- ActivityPub: Add an internal service actor for fetching ActivityPub objects.
- ActivityPub: Optional signing of ActivityPub object fetches.
- Admin API: Endpoint for fetching latest user's statuses
- Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=<email>` for resending account confirmation.
- Pleroma API: Email change endpoint.
- Admin API: Added moderation log
-- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
- Web response cache (currently, enabled for ActivityPub)
-- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
-- ActivityPub: Add ActivityPub actor's `discoverable` parameter.
-- Admin API: Added moderation log filters (user/start date/end date/search/pagination)
- Reverse Proxy: Do not retry failed requests to limit pressure on the peer
### Changed
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
- Admin API: changed json structure for saving config settings.
- RichMedia: parsers and their order are configured in `rich_media` config.
- RichMedia: add the rich media ttl based on image expiration time.
## [1.0.7] - 2019-09-26
### Fixed
- Broken federation on Erlang 22 (previous versions of hackney http client were using an option that got deprecated)
### Changed
- ActivityPub: The first page in inboxes/outboxes is no longer embedded.
## [1.0.6] - 2019-08-14
### Fixed
- MRF: fix use of unserializable keyword lists in describe() implementations
- ActivityPub S2S: POST requests are now signed with `(request-target)` pseudo-header.
## [1.0.5] - 2019-08-13
### Fixed
- Mastodon API: follower/following counters not being nullified, when `hide_follows`/`hide_followers` is set
- Mastodon API: `muted` in the Status entity, using author's account to determine if the thread was muted
- Mastodon API: return the actual profile URL in the Account entity's `url` property when appropriate
- Templates: properly style anchor tags
- Objects being re-embedded to activities after being updated (e.g faved/reposted). Running 'mix pleroma.database prune_objects' again is advised.
- Not being able to access the Mastodon FE login page on private instances
- MRF: ensure that subdomain_match calls are case-insensitive
- Fix internal server error when using the healthcheck API.
### Added
- **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo.
Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules.
- Relays: Added a task to list relay subscriptions.
- MRF: Support for filtering posts based on ActivityStreams vocabulary (`Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`)
- MRF (Simple Policy): Support for wildcard domains.
- Support for wildcard domains in user domain blocks setting.
- Configuration: `quarantined_instances` support wildcard domains.
- Mix Tasks: `mix pleroma.database fix_likes_collections`
- Configuration: `federation_incoming_replies_max_depth` option
### Removed
- Federation: Remove `likes` from objects.
- **Breaking:** ActivityPub: The `accept_blocks` configuration setting.
## [1.0.4] - 2019-08-01
### Fixed
- Invalid SemVer version generation, when the current branch does not have commits ahead of tag/checked out on a tag
## [1.0.3] - 2019-07-31
### Security
- OStatus: eliminate the possibility of a protocol downgrade attack.
- OStatus: prevent following locked accounts, bypassing the approval process.
- TwitterAPI: use CommonAPI to handle remote follows instead of OStatus.
## [1.0.2] - 2019-07-28
### Fixed
- Not being able to pin unlisted posts
- Mastodon API: represent poll IDs as strings
- MediaProxy: fix matching filenames
- MediaProxy: fix filename encoding
- Migrations: fix a sporadic migration failure
- Metadata rendering errors resulting in the entire page being inaccessible
- Federation/MediaProxy not working with instances that have wrong certificate order
- ActivityPub S2S: remote user deletions now work the same as local user deletions.
### Changed
- Configuration: OpenGraph and TwitterCard providers enabled by default
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
## [1.0.1] - 2019-07-14
### Security
- OStatus: fix an object spoofing vulnerability.
## [1.0.0] - 2019-06-29
### Security
- Mastodon API: Fix display names not being sanitized
- Rich media: Do not crawl private IP ranges
### Added
- Digest email for inactive users
- Add a generic settings store for frontends / clients to use.
- Explicit addressing option for posting.
- Optional SSH access mode. (Needs `erlang-ssh` package on some distributions).
- [MongooseIM](https://github.com/esl/MongooseIM) http authentication support.
- LDAP authentication
- External OAuth provider authentication
- Support for building a release using [`mix release`](https://hexdocs.pm/mix/master/Mix.Tasks.Release.html)
- A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc.
- [Prometheus](https://prometheus.io/) metrics
- Support for Mastodon's remote interaction
- Mix Tasks: `mix pleroma.database bump_all_conversations`
- Mix Tasks: `mix pleroma.database remove_embedded_objects`
- Mix Tasks: `mix pleroma.database update_users_following_followers_counts`
- Mix Tasks: `mix pleroma.user toggle_confirmed`
- Mix Tasks: `mix pleroma.config migrate_to_db`
- Mix Tasks: `mix pleroma.config migrate_from_db`
- Federation: Support for `Question` and `Answer` objects
- Federation: Support for reports
- Configuration: `poll_limits` option
- Configuration: `pack_extensions` option
- Configuration: `safe_dm_mentions` option
- Configuration: `link_name` option
- Configuration: `fetch_initial_posts` option
- Configuration: `notify_email` option
- Configuration: Media proxy `whitelist` option
- Configuration: `report_uri` option
- Configuration: `email_notifications` option
- Configuration: `limit_to_local_content` option
- Pleroma API: User subscriptions
- Pleroma API: Healthcheck endpoint
- Pleroma API: `/api/v1/pleroma/mascot` per-user frontend mascot configuration endpoints
- Admin API: Endpoints for listing/revoking invite tokens
- Admin API: Endpoints for making users follow/unfollow each other
- Admin API: added filters (role, tags, email, name) for users endpoint
- Admin API: Endpoints for managing reports
- Admin API: Endpoints for deleting and changing the scope of individual reported statuses
- Admin API: Endpoints to view and change config settings.
- AdminFE: initial release with basic user management accessible at /pleroma/admin/
- Mastodon API: Add chat token to `verify_credentials` response
- Mastodon API: Add background image setting to `update_credentials`
- Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/)
- Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension)
- Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
- Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
- Mastodon API: `POST /api/v1/accounts` (account creation API)
- Mastodon API: [Polls](https://docs.joinmastodon.org/api/rest/polls/)
- ActivityPub C2S: OAuth endpoints
- Metadata: RelMe provider
- OAuth: added support for refresh tokens
- Emoji packs and emoji pack manager
- Object pruning (`mix pleroma.database prune_objects`)
- OAuth: added job to clean expired access tokens
- MRF: Support for rejecting reports from specific instances (`mrf_simple`)
- MRF: Support for stripping avatars and banner images from specific instances (`mrf_simple`)
- MRF: Support for running subchains.
- Configuration: `skip_thread_containment` option
- Configuration: `rate_limit` option. See `Pleroma.Plugs.RateLimiter` documentation for details.
- MRF: Support for filtering out likely spam messages by rejecting posts from new users that contain links.
- Configuration: `ignore_hosts` option
- Configuration: `ignore_tld` option
- Configuration: default syslog tag "Pleroma" is now lowercased to "pleroma"
### Changed
- **Breaking:** bind to 127.0.0.1 instead of 0.0.0.0 by default
- **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
- Thread containment / test for complete visibility will be skipped by default.
- Enforcement of OAuth scopes
- Add multiple use/time expiring invite token
- Restyled OAuth pages to fit with Pleroma's default theme
- Link/mention/hashtag detection is now handled by [auto_linker](https://git.pleroma.social/pleroma/auto_linker)
- NodeInfo: Return `safe_dm_mentions` feature flag
- Federation: Expand the audience of delete activities to all recipients of the deleted object
- Federation: Removed `inReplyToStatusId` from objects
- Configuration: Dedupe enabled by default
- Configuration: Default log level in `prod` environment is now set to `warn`
- Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work.
- Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats.
- Admin API: Move the user related API to `api/pleroma/admin/users`
- Admin API: `POST /api/pleroma/admin/users` will take list of users
- Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change
- Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications`
- Mastodon API: Add `languages` and `registrations` to `/api/v1/instance`
- Mastodon API: Provide plaintext versions of cw/content in the Status entity
- Mastodon API: Add `pleroma.conversation_id`, `pleroma.in_reply_to_account_acct` fields to the Status entity
- Mastodon API: Add `pleroma.tags`, `pleroma.relationship{}`, `pleroma.is_moderator`, `pleroma.is_admin`, `pleroma.confirmation_pending`, `pleroma.hide_followers`, `pleroma.hide_follows`, `pleroma.hide_favorites` fields to the User entity
- Mastodon API: Add `pleroma.show_role`, `pleroma.no_rich_text` fields to the Source subentity
- Mastodon API: Add support for updating `no_rich_text`, `hide_followers`, `hide_follows`, `hide_favorites`, `show_role` in `PATCH /api/v1/update_credentials`
- Mastodon API: Add `pleroma.is_seen` to the Notification entity
- Mastodon API: Add `pleroma.local` to the Status entity
- Mastodon API: Add `preview` parameter to `POST /api/v1/statuses`
- Mastodon API: Add `with_muted` parameter to timeline endpoints
- Mastodon API: Actual reblog hiding instead of a dummy
- Mastodon API: Remove attachment limit in the Status entity
- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints.
- Deps: Updated Cowboy to 2.6
- Deps: Updated Ecto to 3.0.7
- Don't ship finmoji by default, they can be installed as an emoji pack
- Hide deactivated users and their statuses
- Posts which are marked sensitive or tagged nsfw no longer have link previews.
- HTTP connection timeout is now set to 10 seconds.
- Respond with a 404 Not implemented JSON error message when requested API is not implemented
- Rich Media: crawl only https URLs.
### Fixed
- Follow requests don't get 'stuck' anymore.
- Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended.
- Followers counter not being updated when a follower is blocked
- Deactivated users being able to request an access token
- Limit on request body in rich media/relme parsers being ignored resulting in a possible memory leak
- Proper Twitter Card generation instead of a dummy
- Deletions failing for users with a large number of posts
- NodeInfo: Include admins in `staffAccounts`
- ActivityPub: Crashing when requesting empty local user's outbox
- Federation: Handling of objects without `summary` property
- Federation: Add a language tag to activities as required by ActivityStreams 2.0
- Federation: Do not federate avatar/banner if set to default allowing other servers/clients to use their defaults
- Federation: Cope with missing or explicitly nulled address lists
- Federation: Explicitly ensure activities addressed to `as:Public` become addressed to the followers collection
- Federation: Better cope with actors which do not declare a followers collection and use `as:Public` with these semantics
- Federation: Follow requests from remote users who have been blocked will be automatically rejected if appropriate
- MediaProxy: Parse name from content disposition headers even for non-whitelisted types
- MediaProxy: S3 link encoding
- Rich Media: Reject any data which cannot be explicitly encoded into JSON
- Pleroma API: Importing follows from Mastodon 2.8+
- Twitter API: Exposing default scope, `no_rich_text` of the user to anyone
- Twitter API: Returning the `role` object in user entity despite `show_role = false`
- Mastodon API: `/api/v1/favourites` serving only public activities
- Mastodon API: Reblogs having `in_reply_to_id` - `null` even when they are replies
- Mastodon API: Streaming API broadcasting wrong activity id
- Mastodon API: 500 errors when requesting a card for a private conversation
- Mastodon API: Handling of `reblogs` in `/api/v1/accounts/:id/follow`
- Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON
- Mastodon API: Exposing default scope of the user to anyone
- Mastodon API: Make `irreversible` field default to `false` [`POST /api/v1/filters`]
- Mastodon API: Replace missing non-nullable Card attributes with empty strings
- User-Agent is now sent correctly for all HTTP requests.
- MRF: Simple policy now properly delists imported or relayed statuses
## Removed
- Configuration: `config :pleroma, :fe` in favor of the more flexible `config :pleroma, :frontend_configurations`
## [0.9.99999] - 2019-05-31
### Security
- Mastodon API: Fix lists leaking private posts
## [0.9.9999] - 2019-04-05
### Security
- Mastodon API: Fix content warnings skipping HTML sanitization
## [0.9.999] - 2019-03-13
Frontend changes only.
### Added
- Added floating action button for posting status on mobile
### Changed
- Changed user-settings icon to a pencil
### Fixed
- Keyboard shortcuts activating when typing a message
- Gaps when scrolling down on a timeline after showing new
## [0.9.99] - 2019-03-08
### Changed
- Update the frontend to the 0.9.99 tag
### Fixed
- Sign the date header in federation to fix Mastodon federation.
## [0.9.9] - 2019-02-22
This is our first stable release.
diff --git a/README.md b/README.md
index 846442346..dd49822e9 100644
--- a/README.md
+++ b/README.md
@@ -1,80 +1,80 @@
# Pleroma
**Note**: This readme as well as complete documentation is also available at <https://docs-develop.pleroma.social>
## About Pleroma
Pleroma is a microblogging server software that can federate (= exchange messages with) other servers that support the same federation standards (OStatus and ActivityPub). What that means is that you can host a server for yourself or your friends and stay in control of your online identity, but still exchange messages with people on larger servers. Pleroma will federate with all servers that implement either OStatus or ActivityPub, like Friendica, GNU Social, Hubzilla, Mastodon, Misskey, Peertube, and Pixelfed.
Pleroma is written in Elixir, high-performance and can run on small devices like a Raspberry Pi.
For clients it supports the [Mastodon client API](https://docs.joinmastodon.org/api/guidelines/) with Pleroma extensions (see "Pleroma's APIs and Mastodon API extensions" section on <https://docs-develop.pleroma.social>).
- [Client Applications for Pleroma](https://docs-develop.pleroma.social/clients.html)
If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>.
## Installation
**Note:** The guide below may be outdated and in most cases shouldn't be used. Instead check out our [wiki](https://docs.pleroma.social) for platform-specific installation instructions, most likely [Installing on Linux using OTP releases](https://docs.pleroma.social/otp_en.html) is the guide you need.
### OS/Distro packages
Currently Pleroma is not packaged by any OS/Distros, but feel free to reach out to us at [#pleroma-dev on freenode](https://webchat.freenode.net/?channels=%23pleroma-dev) or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma-dev:matrix.org> for assistance. If you want to change default options in your Pleroma package, please **discuss it with us first**.
### Docker
While we don’t provide docker files, other people have written very good ones. Take a look at <https://github.com/angristan/docker-pleroma> or <https://glitch.sh/sn0w/pleroma-docker>.
### Dependencies
-* Postgresql version 9.6 or newer
+* Postgresql version 9.6 or newer, including the contrib modules
* Elixir version 1.7 or newer. If your distribution only has an old version available, check [Elixir’s install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf).
* Build-essential tools
### Configuration
* Run `mix deps.get` to install elixir dependencies.
* Run `mix pleroma.instance gen`. This will ask you questions about your instance and generate a configuration file in `config/generated_config.exs`. Check that and copy it to either `config/dev.secret.exs` or `config/prod.secret.exs`. It will also create a `config/setup_db.psql`, which you should run as the PostgreSQL superuser (i.e., `sudo -u postgres psql -f config/setup_db.psql`). It will create the database, user, and password you gave `mix pleroma.gen.instance` earlier, as well as set up the necessary extensions in the database. PostgreSQL superuser privileges are only needed for this step.
* For these next steps, the default will be to run pleroma using the dev configuration file, `config/dev.secret.exs`. To run them using the prod config file, prefix each command at the shell with `MIX_ENV=prod`. For example: `MIX_ENV=prod mix phx.server`. Documentation for the config can be found at [`docs/config.md`](docs/config.md) in the repository, or at the "Configuration" page on <https://docs-develop.pleroma.social/config.html>
* Run `mix ecto.migrate` to run the database migrations. You will have to do this again after certain updates.
* You can check if your instance is configured correctly by running it with `mix phx.server` and checking the instance info endpoint at `/api/v1/instance`. If it shows your uri, name and email correctly, you are configured correctly. If it shows something like `localhost:4000`, your configuration is probably wrong, unless you are running a local development setup.
* The common and convenient way for adding HTTPS is by using Nginx as a reverse proxy. You can look at example Nginx configuration in `installation/pleroma.nginx`. If you need TLS/SSL certificates for HTTPS, you can look get some for free with letsencrypt: <https://letsencrypt.org/>. The simplest way to obtain and install a certificate is to use [Certbot.](https://certbot.eff.org) Depending on your specific setup, certbot may be able to get a certificate and configure your web server automatically.
## Running
* By default, it listens on port 4000 (TCP), so you can access it on <http://localhost:4000/> (if you are on the same machine). In case of an error it will restart automatically.
### Frontends
Pleroma comes with two frontends. The first one, Pleroma FE, can be reached by normally visiting the site. The other one, based on the Mastodon project, can be found by visiting the /web path of your site.
### As systemd service (with provided .service file)
Example .service file can be found in `installation/pleroma.service`. Copy this to `/etc/systemd/system/`. Running `systemctl enable --now pleroma.service` will run Pleroma and enable startup on boot. Logs can be watched by using `journalctl -fu pleroma.service`.
### As OpenRC service (with provided RC file)
Copy `installation/init.d/pleroma` to `/etc/init.d/pleroma`. You can add it to the services ran by default with: `rc-update add pleroma`
### Standalone/run by other means
Run `mix phx.server` in repository’s root, it will output log into stdout/stderr.
### Using an upstream proxy for federation
Add the following to your `dev.secret.exs` or `prod.secret.exs` if you want to proxify all http requests that Pleroma makes to an upstream proxy server:
```elixir
config :pleroma, :http,
proxy_url: "127.0.0.1:8123"
```
This is useful for running Pleroma inside Tor or I2P.
## Customization and contribution
-The [Pleroma Documentation](https://docs-develop.pleroma.social/readme.html) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project.
+The [Pleroma Documentation](https://docs-develop.pleroma.social) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project.
## Troubleshooting
### No incoming federation
Check that you correctly forward the `host` header to the backend. It is needed to validate signatures.
diff --git a/benchmarks/load_testing/fetcher.ex b/benchmarks/load_testing/fetcher.ex
new file mode 100644
index 000000000..e378c51e7
--- /dev/null
+++ b/benchmarks/load_testing/fetcher.ex
@@ -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
diff --git a/benchmarks/load_testing/generator.ex b/benchmarks/load_testing/generator.ex
new file mode 100644
index 000000000..5c5a5c122
--- /dev/null
+++ b/benchmarks/load_testing/generator.ex
@@ -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
diff --git a/benchmarks/load_testing/helper.ex b/benchmarks/load_testing/helper.ex
new file mode 100644
index 000000000..47b25c65f
--- /dev/null
+++ b/benchmarks/load_testing/helper.ex
@@ -0,0 +1,11 @@
+defmodule Pleroma.LoadTesting.Helper do
+ defmacro __using__(_) do
+ quote do
+ import Ecto.Query
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ defp to_sec(microseconds), do: microseconds / 1_000_000
+ end
+ end
+end
diff --git a/benchmarks/mix/tasks/pleroma/load_testing.ex b/benchmarks/mix/tasks/pleroma/load_testing.ex
new file mode 100644
index 000000000..4fa3eec49
--- /dev/null
+++ b/benchmarks/mix/tasks/pleroma/load_testing.ex
@@ -0,0 +1,134 @@
+defmodule Mix.Tasks.Pleroma.LoadTesting do
+ use Mix.Task
+ use Pleroma.LoadTesting.Helper
+ import Mix.Pleroma
+ import Pleroma.LoadTesting.Generator
+ import Pleroma.LoadTesting.Fetcher
+
+ @shortdoc "Factory for generation data"
+ @moduledoc """
+ Generates data like:
+ - local/remote users
+ - local/remote activities with notifications
+ - direct messages
+ - long thread
+ - non visible posts
+
+ ## Generate data
+ MIX_ENV=benchmark mix pleroma.load_testing --users 20000 --dms 20000 --thread_length 2000
+ MIX_ENV=benchmark mix pleroma.load_testing -u 20000 -d 20000 -t 2000
+
+ Options:
+ - `--users NUMBER` - number of users to generate. Defaults to: 20000. Alias: `-u`
+ - `--dms NUMBER` - number of direct messages to generate. Defaults to: 20000. Alias `-d`
+ - `--thread_length` - number of messages in thread. Defaults to: 2000. ALias `-t`
+ """
+
+ @aliases [u: :users, d: :dms, t: :thread_length]
+ @switches [
+ users: :integer,
+ dms: :integer,
+ thread_length: :integer
+ ]
+ @users_default 20_000
+ @dms_default 1_000
+ @thread_length_default 2_000
+
+ def run(args) do
+ start_pleroma()
+ Pleroma.Config.put([:instance, :skip_thread_containment], true)
+ {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
+
+ users_max = Keyword.get(opts, :users, @users_default)
+ dms_max = Keyword.get(opts, :dms, @dms_default)
+ thread_length = Keyword.get(opts, :thread_length, @thread_length_default)
+
+ clean_tables()
+
+ opts =
+ Keyword.put(opts, :users_max, users_max)
+ |> Keyword.put(:dms_max, dms_max)
+ |> Keyword.put(:thread_length, thread_length)
+
+ generate_users(opts)
+
+ # main user for queries
+ IO.puts("Fetching local main user...")
+
+ {time, user} =
+ :timer.tc(fn ->
+ Repo.one(
+ from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1)
+ )
+ end)
+
+ IO.puts("Fetching main user take #{to_sec(time)} sec.\n")
+
+ IO.puts("Fetching local users...")
+
+ {time, users} =
+ :timer.tc(fn ->
+ Repo.all(
+ from(u in User,
+ where: u.id != ^user.id,
+ where: u.local == true,
+ order_by: fragment("RANDOM()"),
+ limit: 10
+ )
+ )
+ end)
+
+ IO.puts("Fetching local users take #{to_sec(time)} sec.\n")
+
+ IO.puts("Fetching remote users...")
+
+ {time, remote_users} =
+ :timer.tc(fn ->
+ Repo.all(
+ from(u in User,
+ where: u.id != ^user.id,
+ where: u.local == false,
+ order_by: fragment("RANDOM()"),
+ limit: 10
+ )
+ )
+ end)
+
+ IO.puts("Fetching remote users take #{to_sec(time)} sec.\n")
+
+ generate_activities(user, users)
+
+ generate_remote_activities(user, remote_users)
+
+ generate_dms(user, users, opts)
+
+ {:ok, activity} = generate_long_thread(user, users, opts)
+
+ generate_non_visible_message(user, users)
+
+ IO.puts("Users in DB: #{Repo.aggregate(from(u in User), :count, :id)}")
+
+ IO.puts("Activities in DB: #{Repo.aggregate(from(a in Pleroma.Activity), :count, :id)}")
+
+ IO.puts("Objects in DB: #{Repo.aggregate(from(o in Pleroma.Object), :count, :id)}")
+
+ IO.puts(
+ "Notifications in DB: #{Repo.aggregate(from(n in Pleroma.Notification), :count, :id)}"
+ )
+
+ fetch_user(user)
+ query_timelines(user)
+ query_notifications(user)
+ query_dms(user)
+ query_long_thread(user, activity)
+ Pleroma.Config.put([:instance, :skip_thread_containment], false)
+ query_timelines(user)
+ end
+
+ defp clean_tables do
+ IO.puts("Deleting old data...\n")
+ Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;")
+ Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;")
+ Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;")
+ end
+end
diff --git a/config/benchmark.exs b/config/benchmark.exs
new file mode 100644
index 000000000..dd99cf5fd
--- /dev/null
+++ b/config/benchmark.exs
@@ -0,0 +1,84 @@
+use Mix.Config
+
+# We don't run a server during test. If one is required,
+# you can enable the server option below.
+config :pleroma, Pleroma.Web.Endpoint,
+ http: [port: 4001],
+ url: [port: 4001],
+ server: true
+
+# Disable captha for tests
+config :pleroma, Pleroma.Captcha,
+ # It should not be enabled for automatic tests
+ enabled: false,
+ # A fake captcha service for tests
+ method: Pleroma.Captcha.Mock
+
+# Print only warnings and errors during test
+config :logger, level: :warn
+
+config :pleroma, :auth, oauth_consumer_strategies: []
+
+config :pleroma, Pleroma.Upload, filters: [], link_name: false
+
+config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
+
+config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test, enabled: true
+
+config :pleroma, :instance,
+ email: "admin@example.com",
+ notify_email: "noreply@example.com",
+ skip_thread_containment: false,
+ federating: false,
+ external_user_synchronization: false
+
+config :pleroma, :activitypub, sign_object_fetches: false
+
+# Configure your database
+config :pleroma, Pleroma.Repo,
+ adapter: Ecto.Adapters.Postgres,
+ username: "postgres",
+ password: "postgres",
+ database: "pleroma_test",
+ hostname: System.get_env("DB_HOST") || "localhost",
+ pool_size: 10
+
+# Reduce hash rounds for testing
+config :pbkdf2_elixir, rounds: 1
+
+config :tesla, adapter: Tesla.Mock
+
+config :pleroma, :rich_media,
+ enabled: false,
+ ignore_hosts: [],
+ ignore_tld: ["local", "localdomain", "lan"]
+
+config :web_push_encryption, :vapid_details,
+ subject: "mailto:administrator@example.com",
+ public_key:
+ "BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4",
+ private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA"
+
+config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock
+
+config :pleroma_job_queue, disabled: true
+
+config :pleroma, Pleroma.ScheduledActivity,
+ daily_user_limit: 2,
+ total_user_limit: 3,
+ enabled: false
+
+config :pleroma, :rate_limit,
+ search: [{1000, 30}, {1000, 30}],
+ app_account_creation: {10_000, 5},
+ password_reset: {1000, 30}
+
+config :pleroma, :http_security, report_uri: "https://endpoint.com"
+
+config :pleroma, :http, send_user_agent: false
+
+rum_enabled = System.get_env("RUM_ENABLED") == "true"
+config :pleroma, :database, rum_enabled: rum_enabled
+IO.puts("RUM enabled: #{rum_enabled}")
+
+config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
diff --git a/config/config.exs b/config/config.exs
index f4d92102f..a69d41d17 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,604 +1,608 @@
# .i;;;;i.
# iYcviii;vXY:
# .YXi .i1c.
# .YC. . in7.
# .vc. ...... ;1c.
# i7, .. .;1;
# i7, .. ... .Y1i
# ,7v .6MMM@; .YX,
# .7;. ..IMMMMMM1 :t7.
# .;Y. ;$MMMMMM9. :tc.
# vY. .. .nMMM@MMU. ;1v.
# i7i ... .#MM@M@C. .....:71i
# it: .... $MMM@9;.,i;;;i,;tti
# :t7. ..... 0MMMWv.,iii:::,,;St.
# .nC. ..... IMMMQ..,::::::,.,czX.
# .ct: ....... .ZMMMI..,:::::::,,:76Y.
# c2: ......,i..Y$M@t..:::::::,,..inZY
# vov ......:ii..c$MBc..,,,,,,,,,,..iI9i
# i9Y ......iii:..7@MA,..,,,,,,,,,....;AA:
# iIS. ......:ii::..;@MI....,............;Ez.
# .I9. ......:i::::...8M1..................C0z.
# .z9; ......:i::::,.. .i:...................zWX.
# vbv ......,i::::,,. ................. :AQY
# c6Y. .,...,::::,,..:t0@@QY. ................ :8bi
# :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ,
# :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2.
# .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn
# 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi
# 7C...::::::::::::,,,,.. .................... vSi.
# ;1;...,,::::::,......... .................. Yz:
# v97,......... .voC.
# izAotX7777777777777777777777777777777777777777Y7n92:
# .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov.
#
# !!! ATTENTION !!!
# DO NOT EDIT THIS FILE! THIS FILE CONTAINS THE DEFAULT VALUES FOR THE CON-
# FIGURATION! EDIT YOUR SECRET FILE (either prod.secret.exs, dev.secret.exs).
#
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config
# General application configuration
config :pleroma, ecto_repos: [Pleroma.Repo]
config :pleroma, Pleroma.Repo,
types: Pleroma.PostgresTypes,
telemetry_event: [Pleroma.Repo.Instrumenter],
migration_lock: nil
scheduled_jobs =
with digest_config <- Application.get_env(:pleroma, :email_notifications)[:digest],
true <- digest_config[:active] do
[{digest_config[:schedule], {Pleroma.Daemons.DigestEmailDaemon, :perform, []}}]
else
_ -> []
end
-scheduled_jobs =
- scheduled_jobs ++
- [{"0 */6 * * * *", {Pleroma.Web.Websub, :refresh_subscriptions, []}}]
-
config :pleroma, Pleroma.Scheduler,
global: true,
overlap: true,
timezone: :utc,
jobs: scheduled_jobs
config :pleroma, Pleroma.Captcha,
enabled: false,
seconds_valid: 60,
method: Pleroma.Captcha.Kocaptcha
config :pleroma, :hackney_pools,
federation: [
max_connections: 50,
timeout: 150_000
],
media: [
max_connections: 50,
timeout: 150_000
],
upload: [
max_connections: 25,
timeout: 300_000
]
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
# Upload configuration
config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local,
filters: [Pleroma.Upload.Filter.Dedupe],
link_name: true,
proxy_remote: false,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :upload
]
]
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
config :pleroma, Pleroma.Uploaders.S3,
bucket: nil,
streaming_enabled: true,
public_endpoint: "https://s3.amazonaws.com"
config :pleroma, Pleroma.Uploaders.MDII,
cgi: "https://mdii.sakura.ne.jp/mdii-post.cgi",
files: "https://mdii.sakura.ne.jp"
config :pleroma, :emoji,
shortcode_globs: ["/emoji/custom/**/*.png"],
pack_extensions: [".png", ".gif"],
groups: [
# Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md`
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
],
default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json",
shared_pack_cache_seconds_per_file: 60
config :pleroma, :uri_schemes,
valid_schemes: [
"https",
"http",
"dat",
"dweb",
"gopher",
"ipfs",
"ipns",
"irc",
"ircs",
"magnet",
"mailto",
"mumble",
"ssb",
"xmpp"
]
websocket_config = [
path: "/websocket",
serializer: [
{Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
{Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
],
timeout: 60_000,
transport_log: false,
compress: false
]
# Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint,
instrumenters: [Pleroma.Web.Endpoint.Instrumenter],
url: [host: "localhost"],
http: [
ip: {127, 0, 0, 1},
dispatch: [
{:_,
[
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
{Phoenix.Transports.WebSocket,
{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
]}
]
],
protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
signing_salt: "CqaoopA2",
render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
pubsub: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2],
secure_cookie_flag: true,
extra_cookie_attrs: [
"SameSite=Lax"
]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
config :logger, :ex_syslogger,
level: :debug,
ident: "pleroma",
format: "$metadata[$level] $message",
metadata: [:request_id]
config :quack,
level: :warn,
meta: [:all],
webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
config :mime, :types, %{
"application/xml" => ["xml"],
"application/xrd+xml" => ["xrd+xml"],
"application/jrd+json" => ["jrd+json"],
"application/activity+json" => ["activity+json"],
"application/ld+json" => ["activity+json"]
}
config :tesla, adapter: Tesla.Adapter.Hackney
# Configures http settings, upstream proxy etc.
config :pleroma, :http,
proxy_url: nil,
send_user_agent: true,
adapter: [
ssl_options: [
# Workaround for remote server certificate chain issues
partial_chain: &:hackney_connect.partial_chain/1,
# We don't support TLS v1.3 yet
versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
]
]
config :pleroma, :instance,
name: "Pleroma",
email: "example@example.com",
notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5_000,
remote_limit: 100_000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
poll_limits: %{
max_options: 20,
max_option_chars: 200,
min_expiration: 0,
max_expiration: 365 * 24 * 60 * 60
},
registrations_open: true,
federating: true,
federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7,
federation_publisher_modules: [
- Pleroma.Web.ActivityPub.Publisher,
- Pleroma.Web.Websub,
- Pleroma.Web.Salmon
+ Pleroma.Web.ActivityPub.Publisher
],
allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true,
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
allowed_post_formats: [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode"
],
mrf_transparency: true,
mrf_transparency_exclusions: [],
autofollowed_nicknames: [],
max_pinned_statuses: 1,
no_attachment_links: false,
welcome_user_nickname: nil,
welcome_message: nil,
max_report_comment_size: 1000,
safe_dm_mentions: false,
healthcheck: false,
remote_post_retention_days: 90,
skip_thread_containment: true,
limit_to_local_content: :unauthenticated,
dynamic_configuration: false,
user_bio_length: 5000,
user_name_length: 100,
max_account_fields: 10,
max_remote_account_fields: 20,
account_field_name_length: 512,
account_field_value_length: 2048,
external_user_synchronization: true,
extended_nickname_format: false
config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because
# of custom emoji. Issue #275 discusses defanging that somehow.
allow_inline_images: true,
allow_headings: false,
allow_tables: false,
allow_fonts: false,
scrub_policy: [
Pleroma.HTML.Transform.MediaProxy,
Pleroma.HTML.Scrubber.Default
]
config :pleroma, :frontend_configurations,
pleroma_fe: %{
theme: "pleroma-dark",
logo: "/static/logo.png",
background: "/images/city.jpg",
redirectRootNoLogin: "/main/all",
redirectRootLogin: "/main/friends",
showInstanceSpecificPanel: true,
scopeOptionsEnabled: false,
formattingOptionsEnabled: false,
collapseMessageWithSubject: false,
hidePostStats: false,
hideUserStats: false,
scopeCopy: true,
subjectLineBehavior: "email",
alwaysShowSubjectInput: true
},
masto_fe: %{
showInstanceSpecificPanel: true
}
config :pleroma, :assets,
mascots: [
pleroma_fox_tan: %{
url: "/images/pleroma-fox-tan-smol.png",
mime_type: "image/png"
},
pleroma_fox_tan_shy: %{
url: "/images/pleroma-fox-tan-shy.png",
mime_type: "image/png"
}
],
default_mascot: :pleroma_fox_tan
+config :pleroma, :manifest,
+ icons: [
+ %{
+ src: "/static/logo.png",
+ type: "image/png"
+ }
+ ],
+ theme_color: "#282c37",
+ background_color: "#191b22"
+
config :pleroma, :activitypub,
unfollow_blocked: true,
outgoing_blocks: true,
follow_handshake_timeout: 500,
sign_object_fetches: true
config :pleroma, :streamer,
workers: 3,
overflow_workers: 2
config :pleroma, :user, deny_follow_blocked: true
config :pleroma, :mrf_normalize_markup, scrub_policy: Pleroma.HTML.Scrubber.Default
config :pleroma, :mrf_rejectnonpublic,
allow_followersonly: false,
allow_direct: false
config :pleroma, :mrf_hellthread,
delist_threshold: 10,
reject_threshold: 20
config :pleroma, :mrf_simple,
media_removal: [],
media_nsfw: [],
federated_timeline_removal: [],
report_removal: [],
reject: [],
accept: [],
avatar_removal: [],
banner_removal: []
config :pleroma, :mrf_keyword,
reject: [],
federated_timeline_removal: [],
replace: []
config :pleroma, :mrf_subchain, match_actor: %{}
config :pleroma, :mrf_vocabulary,
accept: [],
reject: []
config :pleroma, :rich_media,
enabled: true,
ignore_hosts: [],
ignore_tld: ["local", "localdomain", "lan"],
parsers: [
Pleroma.Web.RichMedia.Parsers.TwitterCard,
Pleroma.Web.RichMedia.Parsers.OGP,
Pleroma.Web.RichMedia.Parsers.OEmbed
],
ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl]
config :pleroma, :media_proxy,
enabled: false,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :media
]
],
whitelist: []
config :pleroma, :chat, enabled: true
config :phoenix, :format_encoders, json: Jason
config :phoenix, :json_library, Jason
config :pleroma, :gopher,
enabled: false,
ip: {0, 0, 0, 0},
port: 9999
config :pleroma, Pleroma.Web.Metadata,
providers: [
Pleroma.Web.Metadata.Providers.OpenGraph,
Pleroma.Web.Metadata.Providers.TwitterCard,
Pleroma.Web.Metadata.Providers.RelMe,
Pleroma.Web.Metadata.Providers.Feed
],
unfurl_nsfw: false
config :pleroma, :suggestions,
enabled: false,
third_party_engine:
"http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-suggestions-api.cgi?{{host}}+{{user}}",
timeout: 300_000,
limit: 40,
web: "https://vinayaka.distsn.org"
config :pleroma, :http_security,
enabled: true,
sts: false,
sts_max_age: 31_536_000,
ct_max_age: 2_592_000,
referrer_policy: "same-origin"
config :cors_plug,
max_age: 86_400,
methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"],
expose: [
"Link",
"X-RateLimit-Reset",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-Request-Id",
"Idempotency-Key"
],
credentials: true,
headers: ["Authorization", "Content-Type", "Idempotency-Key"]
config :pleroma, Pleroma.User,
restricted_nicknames: [
".well-known",
"~",
"about",
"activities",
"api",
"auth",
"check_password",
"dev",
"friend-requests",
"inbox",
"internal",
"main",
"media",
"nodeinfo",
"notice",
"oauth",
"objects",
"ostatus_subscribe",
"pleroma",
"proxy",
"push",
"registration",
"relay",
"settings",
"status",
"tag",
"user-search",
"user_exists",
"users",
"web"
]
config :pleroma, Oban,
repo: Pleroma.Repo,
verbose: false,
prune: {:maxlen, 1500},
queues: [
activity_expiration: 10,
federator_incoming: 50,
federator_outgoing: 50,
web_push: 50,
mailer: 10,
transmogrifier: 20,
scheduled_activities: 10,
background: 5
]
config :pleroma, :workers,
retries: [
federator_incoming: 5,
federator_outgoing: 5
]
config :pleroma, :fetch_initial_posts,
enabled: false,
pages: 5
config :auto_linker,
opts: [
scheme: true,
extra: true,
# TODO: Set to :no_scheme when it works properly
validate_tld: true,
class: false,
strip_prefix: false,
new_window: false,
rel: "ugc"
]
config :pleroma, :ldap,
enabled: System.get_env("LDAP_ENABLED") == "true",
host: System.get_env("LDAP_HOST") || "localhost",
port: String.to_integer(System.get_env("LDAP_PORT") || "389"),
ssl: System.get_env("LDAP_SSL") == "true",
sslopts: [],
tls: System.get_env("LDAP_TLS") == "true",
tlsopts: [],
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
uid: System.get_env("LDAP_UID") || "cn"
config :esshd,
enabled: false
oauth_consumer_strategies =
System.get_env("OAUTH_CONSUMER_STRATEGIES")
|> to_string()
|> String.split()
|> Enum.map(&hd(String.split(&1, ":")))
ueberauth_providers =
for strategy <- oauth_consumer_strategies do
strategy_module_name = "Elixir.Ueberauth.Strategy.#{String.capitalize(strategy)}"
strategy_module = String.to_atom(strategy_module_name)
{String.to_atom(strategy), {strategy_module, [callback_params: ["state"]]}}
end
config :ueberauth,
Ueberauth,
base_path: "/oauth",
providers: ueberauth_providers
config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false
config :pleroma, Pleroma.Emails.UserEmail,
logo: nil,
styling: %{
link_color: "#d8a070",
background_color: "#2C3645",
content_background_color: "#1B2635",
header_color: "#d8a070",
text_color: "#b9b9ba",
text_muted_color: "#b9b9ba"
}
config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics"
config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 25,
total_user_limit: 300,
enabled: true
config :pleroma, :email_notifications,
digest: %{
active: false,
schedule: "0 0 * * 0",
interval: 7,
inactivity_threshold: 7
}
config :pleroma, :oauth2,
token_expires_in: 600,
issue_new_refresh_token: true,
clean_expired_tokens: false,
clean_expired_tokens_interval: 86_400_000
config :pleroma, :database, rum_enabled: false
config :pleroma, :env, Mix.env()
config :http_signatures,
adapter: Pleroma.Signature
config :pleroma, :rate_limit, authentication: {60_000, 15}
config :pleroma, Pleroma.ActivityExpiration, enabled: true
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
config :pleroma, :web_cache_ttl,
activity_pub: nil,
activity_pub_question: 30_000
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
diff --git a/config/description.exs b/config/description.exs
index b007cf69c..70e963399 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -1,2756 +1,2793 @@
use Mix.Config
alias Pleroma.Docs.Generator
websocket_config = [
path: "/websocket",
serializer: [
{Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
{Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
],
timeout: 60_000,
transport_log: false,
compress: false
]
config :pleroma, :config_description, [
%{
group: :pleroma,
key: Pleroma.Upload,
type: :group,
description: "Upload general settings",
children: [
%{
key: :uploader,
type: :module,
description: "Module which will be used for uploads",
suggestions: [
Generator.uploaders_list()
]
},
%{
key: :filters,
type: {:list, :module},
description: "List of filter modules for uploads",
suggestions: [
Generator.filters_list()
]
},
%{
key: :link_name,
type: :boolean,
description:
"If enabled, a name parameter will be added to the url of the upload. For example `https://instance.tld/media/imagehash.png?name=realname.png`"
},
%{
key: :base_url,
type: :string,
description: "Base url for the uploads, needed if you use CDN",
suggestions: [
"https://cdn-host.com"
]
},
%{
key: :proxy_remote,
type: :boolean,
description:
"If enabled, requests to media stored using a remote uploader will be proxied instead of being redirected."
},
%{
key: :proxy_opts,
type: :keyword,
description: "Proxy options, see `Pleroma.ReverseProxy` documentation"
}
]
},
%{
group: :pleroma,
key: Pleroma.Uploaders.Local,
type: :group,
description: "Local uploader-related settings",
children: [
%{
key: :uploads,
type: :string,
description: "Path where user uploads will be saved",
suggestions: [
"uploads"
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Uploaders.S3,
type: :group,
description: "S3 uploader-related settings",
children: [
%{
key: :bucket,
type: :string,
description: "S3 bucket",
suggestions: [
"bucket"
]
},
%{
key: :bucket_namespace,
type: :string,
description: "S3 bucket namespace",
suggestions: ["pleroma"]
},
%{
key: :public_endpoint,
type: :string,
description: "S3 endpoint",
suggestions: ["https://s3.amazonaws.com"]
},
%{
key: :truncated_namespace,
type: :string,
description:
"If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc." <>
" For example, when using CDN to S3 virtual host format, set \"\". At this time, write CNAME to CDN in public_endpoint."
},
%{
key: :streaming_enabled,
type: :boolean,
description:
"Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems."
}
]
},
%{
group: :pleroma,
key: Pleroma.Upload.Filter.Mogrify,
type: :group,
description: "Uploads mogrify filter settings",
children: [
%{
key: :args,
type: [:string, {:list, :string}, {:list, :tuple}],
description: "List of actions for the mogrify command",
suggestions: [
"strip",
["strip", "auto-orient"],
[{"implode", "1"}],
["strip", "auto-orient", {"implode", "1"}]
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Upload.Filter.AnonymizeFilename,
type: :group,
description: "Filter replaces the filename of the upload",
children: [
%{
key: :text,
type: :string,
description:
"Text to replace filenames in links. If no setting, {random}.extension will be used. You can get the original" <>
" filename extension by using {extension}, for example custom-file-name.{extension}",
suggestions: [
"custom-file-name.{extension}",
nil
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Emails.Mailer,
type: :group,
description: "Mailer-related settings",
children: [
%{
key: :adapter,
type: :module,
description:
"One of the mail adapters listed in [Swoosh readme](https://github.com/swoosh/swoosh#adapters)," <>
" or Swoosh.Adapters.Local for in-memory mailbox",
suggestions: [
Swoosh.Adapters.SMTP,
Swoosh.Adapters.Sendgrid,
Swoosh.Adapters.Sendmail,
Swoosh.Adapters.Mandrill,
Swoosh.Adapters.Mailgun,
Swoosh.Adapters.Mailjet,
Swoosh.Adapters.Postmark,
Swoosh.Adapters.SparkPost,
Swoosh.Adapters.AmazonSES,
Swoosh.Adapters.Dyn,
Swoosh.Adapters.SocketLabs,
Swoosh.Adapters.Gmail
]
},
%{
key: :enabled,
type: :boolean,
description: "Allow/disallow send emails"
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :relay,
type: :string,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: ["smtp.gmail.com"]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :username,
type: :string,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: ["pleroma"]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :password,
type: :string,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: ["password"]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :ssl,
type: :boolean,
description: "`Swoosh.Adapters.SMTP` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :tls,
type: :atom,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: [:always, :never, :if_available]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :auth,
type: :atom,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: [:always, :never, :if_available]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :port,
type: :integer,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: [1025]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :retries,
type: :integer,
description: "`Swoosh.Adapters.SMTP` adapter specific setting",
suggestions: [5]
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :no_mx_lookups,
type: :boolean,
description: "`Swoosh.Adapters.SMTP` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.Sendgrid},
key: :api_key,
type: :string,
description: "`Swoosh.Adapters.Sendgrid` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Sendmail},
key: :cmd_path,
type: :string,
description: "`Swoosh.Adapters.Sendmail` adapter specific setting",
suggestions: ["/usr/bin/sendmail"]
},
%{
group: {:subgroup, Swoosh.Adapters.Sendmail},
key: :cmd_args,
type: :string,
description: "`Swoosh.Adapters.Sendmail` adapter specific setting",
suggestions: ["-N delay,failure,success"]
},
%{
group: {:subgroup, Swoosh.Adapters.Sendmail},
key: :qmail,
type: :boolean,
description: "`Swoosh.Adapters.Sendmail` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.Mandrill},
key: :api_key,
type: :string,
description: "`Swoosh.Adapters.Mandrill` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailgun},
key: :api_key,
type: :string,
description: "`Swoosh.Adapters.Mailgun` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailgun},
key: :domain,
type: :string,
description: "`Swoosh.Adapters.Mailgun` adapter specific setting",
suggestions: ["pleroma.com"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailjet},
key: :api_key,
type: :string,
description: "`Swoosh.Adapters.Mailjet` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Mailjet},
key: :secret,
type: :string,
description: "`Swoosh.Adapters.Mailjet` adapter specific setting",
suggestions: ["my-secret-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Postmark},
key: :api_key,
type: :string,
description: "`Swoosh.Adapters.Postmark` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.SparkPost},
key: :api_key,
type: :string,
description: "`Swoosh.Adapters.SparkPost` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.SparkPost},
key: :endpoint,
type: :string,
description: "`Swoosh.Adapters.SparkPost` adapter specific setting",
suggestions: ["https://api.sparkpost.com/api/v1"]
},
%{
group: {:subgroup, Swoosh.Adapters.AmazonSES},
key: :region,
type: {:string},
description: "`Swoosh.Adapters.AmazonSES` adapter specific setting",
suggestions: ["us-east-1", "us-east-2"]
},
%{
group: {:subgroup, Swoosh.Adapters.AmazonSES},
key: :access_key,
type: :string,
description: "`Swoosh.Adapters.AmazonSES` adapter specific setting",
suggestions: ["aws-access-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.AmazonSES},
key: :secret,
type: :string,
description: "`Swoosh.Adapters.AmazonSES` adapter specific setting",
suggestions: ["aws-secret-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.Dyn},
key: :api_key,
type: :string,
description: "`Swoosh.Adapters.Dyn` adapter specific setting",
suggestions: ["my-api-key"]
},
%{
group: {:subgroup, Swoosh.Adapters.SocketLabs},
key: :server_id,
type: :string,
description: "`Swoosh.Adapters.SocketLabs` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.SocketLabs},
key: :api_key,
type: :string,
description: "`Swoosh.Adapters.SocketLabs` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.Gmail},
key: :access_token,
type: :string,
description: "`Swoosh.Adapters.Gmail` adapter specific setting"
}
]
},
%{
group: :pleroma,
key: :uri_schemes,
type: :group,
description: "URI schemes related settings",
children: [
%{
key: :valid_schemes,
type: {:list, :string},
description: "List of the scheme part that is considered valid to be an URL",
suggestions: [
[
"https",
"http",
"dat",
"dweb",
"gopher",
"ipfs",
"ipns",
"irc",
"ircs",
"magnet",
"mailto",
"mumble",
"ssb",
"xmpp"
]
]
}
]
},
%{
group: :pleroma,
key: :instance,
type: :group,
description: "Instance-related settings",
children: [
%{
key: :name,
type: :string,
description: "Name of the instance",
suggestions: [
"Pleroma"
]
},
%{
key: :email,
type: :string,
description: "Email used to reach an Administrator/Moderator of the instance",
suggestions: [
"email@example.com"
]
},
%{
key: :notify_email,
type: :string,
description: "Email used for notifications",
suggestions: [
"notify@example.com"
]
},
%{
key: :description,
type: :string,
description: "The instance's description, can be seen in nodeinfo and /api/v1/instance",
suggestions: [
"Very cool instance"
]
},
%{
key: :limit,
type: :integer,
description: "Posts character limit (CW/Subject included in the counter)",
suggestions: [
5_000
]
},
%{
key: :remote_limit,
type: :integer,
description: "Hard character limit beyond which remote posts will be dropped",
suggestions: [
100_000
]
},
%{
key: :upload_limit,
type: :integer,
description: "File size limit of uploads (except for avatar, background, banner)",
suggestions: [
16_000_000
]
},
%{
key: :avatar_upload_limit,
type: :integer,
description: "File size limit of user's profile avatars",
suggestions: [
2_000_000
]
},
%{
key: :background_upload_limit,
type: :integer,
description: "File size limit of user's profile backgrounds",
suggestions: [
4_000_000
]
},
%{
key: :banner_upload_limit,
type: :integer,
description: "File size limit of user's profile banners",
suggestions: [
4_000_000
]
},
%{
key: :poll_limits,
type: :map,
description: "A map with poll limits for local polls",
suggestions: [
%{
max_options: 20,
max_option_chars: 200,
min_expiration: 0,
max_expiration: 31_536_000
}
],
children: [
%{
key: :max_options,
type: :integer,
description: "Maximum number of options",
suggestions: [20]
},
%{
key: :max_option_chars,
type: :integer,
description: "Maximum number of characters per option",
suggestions: [200]
},
%{
key: :min_expiration,
type: :integer,
description: "Minimum expiration time (in seconds)",
suggestions: [0]
},
%{
key: :max_expiration,
type: :integer,
description: "Maximum expiration time (in seconds)",
suggestions: [3600]
}
]
},
%{
key: :registrations_open,
type: :boolean,
description: "Enable registrations for anyone, invitations can be enabled when false"
},
%{
key: :invites_enabled,
type: :boolean,
description: "Enable user invitations for admins (depends on registrations_open: false)"
},
%{
key: :account_activation_required,
type: :boolean,
description: "Require users to confirm their emails before signing in"
},
%{
key: :federating,
type: :boolean,
description: "Enable federation with other instances"
},
%{
key: :federation_incoming_replies_max_depth,
type: :integer,
description:
"Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while" <>
" fetching very long threads. If set to nil, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes",
suggestions: [
100
]
},
%{
key: :federation_reachability_timeout_days,
type: :integer,
description:
"Timeout (in days) of each external federation target being unreachable prior to pausing federating to it",
suggestions: [
7
]
},
%{
key: :federation_publisher_modules,
type: [:list, :module],
description: "List of modules for federation publishing",
suggestions: [
- Pleroma.Web.ActivityPub.Publisher,
- Pleroma.Web.Websub,
- Pleroma.Web.Salmo
+ Pleroma.Web.ActivityPub.Publisher
]
},
%{
key: :allow_relay,
type: :boolean,
description: "Enable Pleroma's Relay, which makes it possible to follow a whole instance"
},
%{
key: :rewrite_policy,
type: {:list, :module},
description: "A list of MRF policies enabled",
suggestions: [
Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
Generator.mrf_list()
]
},
%{
key: :public,
type: :boolean,
description:
"Makes the client API in authentificated mode-only except for user-profiles." <>
" Useful for disabling the Local Timeline and The Whole Known Network"
},
%{
key: :quarantined_instances,
type: {:list, :string},
description:
"List of ActivityPub instances where private(DMs, followers-only) activities will not be send",
suggestions: [
"quarantined.com",
"*.quarantined.com"
]
},
%{
key: :managed_config,
type: :boolean,
description:
"Whenether the config for pleroma-fe is configured in this config or in static/config.json"
},
%{
key: :static_dir,
type: :string,
description: "Instance static directory",
suggestions: [
"instance/static/"
]
},
%{
key: :allowed_post_formats,
type: {:list, :string},
description: "MIME-type list of formats allowed to be posted (transformed into HTML)",
suggestions: [
[
"text/plain",
"text/html",
"text/markdown",
"text/bbcode"
]
]
},
%{
key: :mrf_transparency,
type: :boolean,
description:
"Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
},
%{
key: :mrf_transparency_exclusions,
type: {:list, :string},
description:
"Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value",
suggestions: [
["exclusion.com"]
]
},
%{
key: :extended_nickname_format,
type: :boolean,
description:
"Set to true to use extended local nicknames format (allows underscores/dashes)." <>
" This will break federation with older software for theses nicknames"
},
%{
key: :max_pinned_statuses,
type: :integer,
description: "The maximum number of pinned statuses. 0 will disable the feature",
suggestions: [
0,
1,
3
]
},
%{
key: :autofollowed_nicknames,
type: {:list, :string},
description:
"Set to nicknames of (local) users that every new user should automatically follow",
suggestions: [
"lain",
"kaniini",
"lanodan",
"rinpatch"
]
},
%{
key: :no_attachment_links,
type: :boolean,
description:
"Set to true to disable automatically adding attachment link text to statuses"
},
%{
key: :welcome_message,
type: :string,
description:
"A message that will be send to a newly registered users as a direct message",
suggestions: [
"Hi, @username! Welcome to the board!",
nil
]
},
%{
key: :welcome_user_nickname,
type: :string,
description: "The nickname of the local user that sends the welcome message",
suggestions: [
"lain",
nil
]
},
%{
key: :max_report_comment_size,
type: :integer,
description: "The maximum size of the report comment (Default: 1000)",
suggestions: [
1_000
]
},
%{
key: :safe_dm_mentions,
type: :boolean,
description:
"If set to true, only mentions at the beginning of a post will be used to address people in direct messages." <>
" This is to prevent accidental mentioning of people when talking about them (e.g. \"@friend hey i really don't like @enemy\")." <>
" Default: false"
},
%{
key: :healthcheck,
type: :boolean,
description: "If set to true, system data will be shown on /api/pleroma/healthcheck"
},
%{
key: :remote_post_retention_days,
type: :integer,
description:
"The default amount of days to retain remote posts when pruning the database",
suggestions: [
90
]
},
%{
key: :user_bio_length,
type: :integer,
description: "A user bio maximum length (default: 5000)",
suggestions: [
5_000
]
},
%{
key: :user_name_length,
type: :integer,
description: "A user name maximum length (default: 100)",
suggestions: [
100
]
},
%{
key: :skip_thread_containment,
type: :boolean,
description: "Skip filter out broken threads. The default is true"
},
%{
key: :limit_to_local_content,
type: [:atom, false],
description:
"Limit unauthenticated users to search for local statutes and users only. The default is :unauthenticated ",
suggestions: [
:unauthenticated,
:all,
false
]
},
%{
key: :dynamic_configuration,
type: :boolean,
description:
"Allow transferring configuration to DB with the subsequent customization from Admin api. Defaults to `false`"
},
%{
key: :max_account_fields,
type: :integer,
description: "The maximum number of custom fields in the user profile (default: 10)",
suggestions: [
10
]
},
%{
key: :max_remote_account_fields,
type: :integer,
description:
"The maximum number of custom fields in the remote user profile (default: 20)",
suggestions: [
20
]
},
%{
key: :account_field_name_length,
type: :integer,
description: "An account field name maximum length (default: 512)",
suggestions: [
512
]
},
%{
key: :account_field_value_length,
type: :integer,
description: "An account field value maximum length (default: 2048)",
suggestions: [
2048
]
},
%{
key: :external_user_synchronization,
type: :boolean,
description: "Enabling following/followers counters synchronization for external users"
}
]
},
%{
group: :logger,
type: :group,
description: "Logger-related settings",
children: [
%{
key: :backends,
type: [:atom, :tuple, :module],
description:
"Where logs will be send, :console - send logs to stdout, {ExSyslogger, :ex_syslogger} - to syslog, Quack.Logger - to Slack.",
suggestions: [[:console, {ExSyslogger, :ex_syslogger}, Quack.Logger]]
}
]
},
%{
group: :logger,
type: :group,
key: :ex_syslogger,
description: "ExSyslogger-related settings",
children: [
%{
key: :level,
type: :atom,
description: "Log level",
suggestions: [:debug, :info, :warn, :error]
},
%{
key: :ident,
type: :string,
description:
"A string that's prepended to every message, and is typically set to the app name",
suggestions: ["pleroma"]
},
%{
key: :format,
type: :string,
description: "It defaults to \"$date $time [$level] $levelpad$node $metadata $message\"",
suggestions: ["$metadata[$level] $message"]
},
%{
key: :metadata,
type: {:list, :atom},
suggestions: [[:request_id]]
}
]
},
%{
group: :logger,
type: :group,
key: :console,
description: "Console logger settings",
children: [
%{
key: :level,
type: :atom,
description: "Log level",
suggestions: [:debug, :info, :warn, :error]
},
%{
key: :format,
type: :string,
description: "It defaults to \"$date $time [$level] $levelpad$node $metadata $message\"",
suggestions: ["$metadata[$level] $message"]
},
%{
key: :metadata,
type: {:list, :atom},
suggestions: [[:request_id]]
}
]
},
%{
group: :quack,
type: :group,
description: "Quack-related settings",
children: [
%{
key: :level,
type: :atom,
description: "Log level",
suggestions: [:debug, :info, :warn, :error]
},
%{
key: :meta,
type: {:list, :atom},
description: "Configure which metadata you want to report on",
suggestions: [
:application,
:module,
:file,
:function,
:line,
:pid,
:crash_reason,
:initial_call,
:registered_name,
:all,
:none
]
},
%{
key: :webhook_url,
type: :string,
description: "Configure the Slack incoming webhook",
suggestions: ["https://hooks.slack.com/services/YOUR-KEY-HERE"]
}
]
},
%{
group: :pleroma,
key: :frontend_configurations,
type: :group,
description: "A keyword list that keeps the configuration data for any kind of frontend",
children: [
%{
key: :pleroma_fe,
type: :map,
description: "Settings for Pleroma FE",
suggestions: [
%{
theme: "pleroma-dark",
logo: "/static/logo.png",
background: "/images/city.jpg",
redirectRootNoLogin: "/main/all",
redirectRootLogin: "/main/friends",
showInstanceSpecificPanel: true,
scopeOptionsEnabled: false,
formattingOptionsEnabled: false,
collapseMessageWithSubject: false,
hidePostStats: false,
hideUserStats: false,
scopeCopy: true,
subjectLineBehavior: "email",
alwaysShowSubjectInput: true
}
],
children: [
%{
key: :theme,
type: :string,
description: "Which theme to use, they are defined in styles.json",
suggestions: ["pleroma-dark"]
},
%{
key: :logo,
type: :string,
description: "URL of the logo, defaults to Pleroma's logo",
suggestions: ["/static/logo.png"]
},
%{
key: :background,
type: :string,
description:
"URL of the background, unless viewing a user profile with a background that is set",
suggestions: ["/images/city.jpg"]
},
%{
key: :redirectRootNoLogin,
type: :string,
description:
"relative URL which indicates where to redirect when a user isn't logged in",
suggestions: ["/main/all"]
},
%{
key: :redirectRootLogin,
type: :string,
description:
"relative URL which indicates where to redirect when a user is logged in",
suggestions: ["/main/friends"]
},
%{
key: :showInstanceSpecificPanel,
type: :boolean,
description: "Whenether to show the instance's specific panel"
},
%{
key: :scopeOptionsEnabled,
type: :boolean,
description: "Enable setting an notice visibility and subject/CW when posting"
},
%{
key: :formattingOptionsEnabled,
type: :boolean,
description:
"Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to :instance, allowed_post_formats"
},
%{
key: :collapseMessageWithSubject,
type: :boolean,
description:
"When a message has a subject(aka Content Warning), collapse it by default"
},
%{
key: :hidePostStats,
type: :boolean,
description: "Hide notices statistics(repeats, favorites, ...)"
},
%{
key: :hideUserStats,
type: :boolean,
description:
"Hide profile statistics(posts, posts per day, followers, followings, ...)"
},
%{
key: :scopeCopy,
type: :boolean,
description: "Copy the scope (private/unlisted/public) in replies to posts by default"
},
%{
key: :subjectLineBehavior,
type: :string,
description: "Allows changing the default behaviour of subject lines in replies.
`email`: Copy and preprend re:, as in email,
`masto`: Copy verbatim, as in Mastodon,
`noop`: Don't copy the subjec",
suggestions: ["email", "masto", "noop"]
},
%{
key: :alwaysShowSubjectInput,
type: :boolean,
description: "When set to false, auto-hide the subject field when it's empty"
}
]
},
%{
key: :masto_fe,
type: :map,
description: "Settings for Masto FE",
suggestions: [
%{
showInstanceSpecificPanel: true
}
],
children: [
%{
key: :showInstanceSpecificPanel,
type: :boolean,
description: "Whenether to show the instance's specific panel"
}
]
}
]
},
%{
group: :pleroma,
key: :assets,
type: :group,
description:
"This section configures assets to be used with various frontends. Currently the only option relates to mascots on the mastodon frontend",
children: [
%{
key: :mascots,
type: :keyword,
description:
"Keyword of mascots, each element MUST contain both a url and a mime_type key",
suggestions: [
[
pleroma_fox_tan: %{
url: "/images/pleroma-fox-tan-smol.png",
mime_type: "image/png"
},
pleroma_fox_tan_shy: %{
url: "/images/pleroma-fox-tan-shy.png",
mime_type: "image/png"
}
]
]
},
%{
key: :default_mascot,
type: :atom,
description:
"This will be used as the default mascot on MastoFE (default: :pleroma_fox_tan)",
suggestions: [
:pleroma_fox_tan
]
}
]
},
+ %{
+ group: :pleroma,
+ key: :manifest,
+ type: :group,
+ description:
+ "This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE",
+ children: [
+ %{
+ key: :icons,
+ type: {:list, :map},
+ description: "Describe the icons of the app",
+ suggestion: [
+ %{
+ src: "/static/logo.png"
+ },
+ %{
+ src: "/static/icon.png",
+ type: "image/png"
+ },
+ %{
+ src: "/static/icon.ico",
+ sizes: "72x72 96x96 128x128 256x256"
+ }
+ ]
+ },
+ %{
+ key: :theme_color,
+ type: :string,
+ description: "Describe the theme color of the app",
+ suggestions: ["#282c37", "mediumpurple"]
+ },
+ %{
+ key: :background_color,
+ type: :string,
+ description: "Describe the background color of the app",
+ suggestions: ["#191b22", "aliceblue"]
+ }
+ ]
+ },
%{
group: :pleroma,
key: :mrf_simple,
type: :group,
description: "Message Rewrite Facility",
children: [
%{
key: :media_removal,
type: {:list, :string},
description: "List of instances to remove medias from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :media_nsfw,
type: {:list, :string},
description: "List of instances to put medias as NSFW(sensitive) from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :federated_timeline_removal,
type: {:list, :string},
description:
"List of instances to remove from Federated (aka The Whole Known Network) Timeline",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :reject,
type: {:list, :string},
description: "List of instances to reject any activities from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :accept,
type: {:list, :string},
description: "List of instances to accept any activities from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :report_removal,
type: {:list, :string},
description: "List of instances to reject reports from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :avatar_removal,
type: {:list, :string},
description: "List of instances to strip avatars from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :banner_removal,
type: {:list, :string},
description: "List of instances to strip banners from",
suggestions: ["example.com", "*.example.com"]
}
]
},
%{
group: :pleroma,
key: :mrf_subchain,
type: :group,
description:
"This policy processes messages through an alternate pipeline when a given message matches certain criteria." <>
" All criteria are configured as a map of regular expressions to lists of policy modules.",
children: [
%{
key: :match_actor,
type: :map,
description: "Matches a series of regular expressions against the actor field",
suggestions: [
%{
~r/https:\/\/example.com/s => [Pleroma.Web.ActivityPub.MRF.DropPolicy]
}
]
}
]
},
%{
group: :pleroma,
key: :mrf_rejectnonpublic,
type: :group,
children: [
%{
key: :allow_followersonly,
type: :boolean,
description: "whether to allow followers-only posts"
},
%{
key: :allow_direct,
type: :boolean,
description: "whether to allow direct messages"
}
]
},
%{
group: :pleroma,
key: :mrf_hellthread,
type: :group,
description: "Block messages with too much mentions",
children: [
%{
key: :delist_threshold,
type: :integer,
description:
"Number of mentioned users after which the message gets delisted (the message can still be seen, " <>
" but it will not show up in public timelines and mentioned users won't get notifications about it). Set to 0 to disable",
suggestions: [10]
},
%{
key: :reject_threshold,
type: :integer,
description:
"Number of mentioned users after which the messaged gets rejected. Set to 0 to disable",
suggestions: [20]
}
]
},
%{
group: :pleroma,
key: :mrf_keyword,
type: :group,
description: "Reject or Word-Replace messages with a keyword or regex",
children: [
%{
key: :reject,
type: [:string, :regex],
description:
"A list of patterns which result in message being rejected, each pattern can be a string or a regular expression",
suggestions: ["foo", ~r/foo/iu]
},
%{
key: :federated_timeline_removal,
type: [:string, :regex],
description:
"A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a regular expression",
suggestions: ["foo", ~r/foo/iu]
},
%{
key: :replace,
type: [{:string, :string}, {:regex, :string}],
description:
"A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a regular expression",
suggestions: [{"foo", "bar"}, {~r/foo/iu, "bar"}]
}
]
},
%{
group: :pleroma,
key: :mrf_mention,
type: :group,
description: "Block messages which mention a user",
children: [
%{
key: :actors,
type: {:list, :string},
description: "A list of actors, for which to drop any posts mentioning",
suggestions: [["actor1", "actor2"]]
}
]
},
%{
group: :pleroma,
key: :mrf_vocabulary,
type: :group,
description: "Filter messages which belong to certain activity vocabularies",
children: [
%{
key: :accept,
type: {:list, :string},
description:
"A list of ActivityStreams terms to accept. If empty, all supported messages are accepted",
suggestions: [["Create", "Follow", "Mention", "Announce", "Like"]]
},
%{
key: :reject,
type: {:list, :string},
description:
"A list of ActivityStreams terms to reject. If empty, no messages are rejected",
suggestions: [["Create", "Follow", "Mention", "Announce", "Like"]]
}
]
},
# %{
# group: :pleroma,
# key: :mrf_user_allowlist,
# type: :group,
# description:
# "The keys in this section are the domain names that the policy should apply to." <>
# " Each key should be assigned a list of users that should be allowed through by their ActivityPub ID",
# children: [
# ["example.org": ["https://example.org/users/admin"]],
# suggestions: [
# ["example.org": ["https://example.org/users/admin"]]
# ]
# ]
# },
%{
group: :pleroma,
key: :media_proxy,
type: :group,
description: "Media proxy",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables proxying of remote media to the instance's proxy"
},
%{
key: :base_url,
type: :string,
description:
"The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts",
suggestions: ["https://example.com"]
},
%{
key: :proxy_opts,
type: :keyword,
description: "Options for Pleroma.ReverseProxy",
suggestions: [[max_body_length: 25 * 1_048_576, redirect_on_failure: false]]
},
%{
key: :whitelist,
type: {:list, :string},
description: "List of domains to bypass the mediaproxy",
suggestions: ["example.com"]
}
]
},
%{
group: :pleroma,
key: :gopher,
type: :group,
description: "Gopher settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables the gopher interface"
},
%{
key: :ip,
type: :tuple,
description: "IP address to bind to",
suggestions: [{0, 0, 0, 0}]
},
%{
key: :port,
type: :integer,
description: "Port to bind to",
suggestions: [9999]
},
%{
key: :dstport,
type: :integer,
description: "Port advertised in urls (optional, defaults to port)",
suggestions: [9999]
}
]
},
%{
group: :pleroma,
key: Pleroma.Web.Endpoint,
type: :group,
description: "Phoenix endpoint configuration",
children: [
%{
key: :http,
type: :keyword,
description: "http protocol configuration",
suggestions: [
[port: 8080, ip: {127, 0, 0, 1}]
],
children: [
%{
key: :dispatch,
type: {:list, :tuple},
description: "dispatch settings",
suggestions: [
[
{:_,
[
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
{Phoenix.Transports.WebSocket,
{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
]}
# end copied from config.exs
]
]
},
%{
key: :ip,
type: :tuple,
description: "ip",
suggestions: [
{0, 0, 0, 0}
]
},
%{
key: :port,
type: :integer,
description: "port",
suggestions: [
2020
]
}
]
},
%{
key: :url,
type: :keyword,
description: "configuration for generating urls",
suggestions: [
[host: "example.com", port: 2020, scheme: "https"]
],
children: [
%{
key: :host,
type: :string,
description: "Host",
suggestions: [
"example.com"
]
},
%{
key: :port,
type: :integer,
description: "port",
suggestions: [
2020
]
},
%{
key: :scheme,
type: :string,
description: "Scheme",
suggestions: [
"https",
"https"
]
}
]
},
%{
key: :instrumenters,
type: {:list, :module},
suggestions: [Pleroma.Web.Endpoint.Instrumenter]
},
%{
key: :protocol,
type: :string,
suggestions: ["https"]
},
%{
key: :secret_key_base,
type: :string,
suggestions: ["aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl"]
},
%{
key: :signing_salt,
type: :string,
suggestions: ["CqaoopA2"]
},
%{
key: :render_errors,
type: :keyword,
suggestions: [[view: Pleroma.Web.ErrorView, accepts: ~w(json)]],
children: [
%{
key: :view,
type: :module,
suggestions: [Pleroma.Web.ErrorView]
},
%{
key: :accepts,
type: {:list, :string},
suggestions: ["json"]
}
]
},
%{
key: :pubsub,
type: :keyword,
suggestions: [[name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]],
children: [
%{
key: :name,
type: :module,
suggestions: [Pleroma.PubSub]
},
%{
key: :adapter,
type: :module,
suggestions: [Phoenix.PubSub.PG2]
}
]
},
%{
key: :secure_cookie_flag,
type: :boolean
},
%{
key: :extra_cookie_attrs,
type: {:list, :string},
suggestions: ["SameSite=Lax"]
}
]
},
%{
group: :pleroma,
key: :activitypub,
type: :group,
description: "ActivityPub-related settings",
children: [
%{
key: :unfollow_blocked,
type: :boolean,
description: "Whether blocks result in people getting unfollowed"
},
%{
key: :outgoing_blocks,
type: :boolean,
description: "Whether to federate blocks to other instances"
},
%{
key: :sign_object_fetches,
type: :boolean,
description: "Sign object fetches with HTTP signatures"
},
%{
key: :follow_handshake_timeout,
type: :integer,
description: "Following handshake timeout",
suggestions: [500]
}
]
},
%{
group: :pleroma,
key: :http_security,
type: :group,
description: "HTTP security settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Whether the managed content security policy is enabled"
},
%{
key: :sts,
type: :boolean,
description: "Whether to additionally send a Strict-Transport-Security header"
},
%{
key: :sts_max_age,
type: :integer,
description: "The maximum age for the Strict-Transport-Security header if sent",
suggestions: [31_536_000]
},
%{
key: :ct_max_age,
type: :integer,
description: "The maximum age for the Expect-CT header if sent",
suggestions: [2_592_000]
},
%{
key: :referrer_policy,
type: :string,
description: "The referrer policy to use, either \"same-origin\" or \"no-referrer\"",
suggestions: ["same-origin", "no-referrer"]
},
%{
key: :report_uri,
type: :string,
description: "Adds the specified url to report-uri and report-to group in CSP header",
suggestions: ["https://example.com/report-uri"]
}
]
},
%{
group: :web_push_encryption,
key: :vapid_details,
type: :group,
description:
"Web Push Notifications configuration. You can use the mix task mix web_push.gen.keypair to generate it",
children: [
%{
key: :subject,
type: :string,
description:
"a mailto link for the administrative contact." <>
" It's best if this email is not a personal email address, but rather a group email so that if a person leaves an organization," <>
" is unavailable for an extended period, or otherwise can't respond, someone else on the list can",
suggestions: ["Subject"]
},
%{
key: :public_key,
type: :string,
description: "VAPID public key",
suggestions: ["Public key"]
},
%{
key: :private_key,
type: :string,
description: "VAPID private keyn",
suggestions: ["Private key"]
}
]
},
%{
group: :pleroma,
key: Pleroma.Captcha,
type: :group,
description: "Captcha-related settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Whether the captcha should be shown on registration"
},
%{
key: :method,
type: :module,
description: "The method/service to use for captcha",
suggestions: [Pleroma.Captcha.Kocaptcha]
},
%{
key: :seconds_valid,
type: :integer,
description: "The time in seconds for which the captcha is valid",
suggestions: [60]
}
]
},
%{
group: :pleroma,
key: Pleroma.Captcha.Kocaptcha,
type: :group,
description:
"Kocaptcha is a very simple captcha service with a single API endpoint, the source code is" <>
" here: https://github.com/koto-bank/kocaptcha. The default endpoint https://captcha.kotobank.ch is hosted by the developer",
children: [
%{
key: :endpoint,
type: :string,
description: "the kocaptcha endpoint to use",
suggestions: ["https://captcha.kotobank.ch"]
}
]
},
%{
group: :pleroma,
type: :group,
description:
"Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the 'admin_token' parameter",
children: [
%{
key: :admin_token,
type: :string,
description: "Token",
suggestions: ["some_random_token"]
}
]
},
%{
group: :pleroma_job_queue,
key: :queues,
type: :group,
description: "[Deprecated] Replaced with `Oban`/`:queues` (keeping the same format)"
},
%{
group: :pleroma,
key: Pleroma.Web.Federator.RetryQueue,
type: :group,
description: "[Deprecated] See `Oban` and `:workers` sections for configuration notes",
children: [
%{
key: :max_retries,
type: :integer,
description: "[Deprecated] Replaced as `Oban`/`:queues`/`:outgoing_federation` value"
}
]
},
%{
group: :pleroma,
key: Oban,
type: :group,
description: """
[Oban](https://github.com/sorentwo/oban) asynchronous job processor configuration.
Note: if you are running PostgreSQL in [`silent_mode`](https://postgresqlco.nf/en/doc/param/silent_mode?version=9.1),
it's advised to set [`log_destination`](https://postgresqlco.nf/en/doc/param/log_destination?version=9.1) to `syslog`,
otherwise `postmaster.log` file may grow because of "you don't own a lock of type ShareLock" warnings
(see https://github.com/sorentwo/oban/issues/52).
""",
children: [
%{
key: :repo,
type: :module,
description: "Application's Ecto repo",
suggestions: [Pleroma.Repo]
},
%{
key: :verbose,
type: :boolean,
description: "Logs verbose mode"
},
%{
key: :prune,
type: [:atom, :tuple],
description:
"Non-retryable jobs [pruning settings](https://github.com/sorentwo/oban#pruning)",
suggestions: [:disabled, {:maxlen, 1500}, {:maxage, 60 * 60}]
},
%{
key: :queues,
type: :keyword,
description:
"Background jobs queues (keys: queues, values: max numbers of concurrent jobs)",
suggestions: [
[
activity_expiration: 10,
background: 5,
federator_incoming: 50,
federator_outgoing: 50,
mailer: 10,
scheduled_activities: 10,
transmogrifier: 20,
web_push: 50
]
],
children: [
%{
key: :activity_expiration,
type: :integer,
description: "Activity expiration queue",
suggestions: [10]
},
%{
key: :background,
type: :integer,
description: "Background queue",
suggestions: [5]
},
%{
key: :federator_incoming,
type: :integer,
description: "Incoming federation queue",
suggestions: [50]
},
%{
key: :federator_outgoing,
type: :integer,
description: "Outgoing federation queue",
suggestions: [50]
},
%{
key: :mailer,
type: :integer,
description: "Email sender queue, see Pleroma.Emails.Mailer",
suggestions: [10]
},
%{
key: :scheduled_activities,
type: :integer,
description: "Scheduled activities queue, see Pleroma.ScheduledActivities",
suggestions: [10]
},
%{
key: :transmogrifier,
type: :integer,
description: "Transmogrifier queue",
suggestions: [20]
},
%{
key: :web_push,
type: :integer,
description: "Web push notifications queue",
suggestions: [50]
}
]
}
]
},
%{
group: :pleroma,
key: :workers,
type: :group,
description: "Includes custom worker options not interpretable directly by `Oban`",
children: [
%{
key: :retries,
type: :keyword,
description: "Max retry attempts for failed jobs, per `Oban` queue",
suggestions: [
[
federator_incoming: 5,
federator_outgoing: 5
]
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Web.Metadata,
type: :group,
decsription: "Metadata-related settings",
children: [
%{
key: :providers,
type: {:list, :module},
description: "List of metadata providers to enable",
suggestions: [
[
Pleroma.Web.Metadata.Providers.OpenGraph,
Pleroma.Web.Metadata.Providers.TwitterCard,
Pleroma.Web.Metadata.Providers.RelMe
]
]
},
%{
key: :unfurl_nsfw,
type: :boolean,
description: "If set to true nsfw attachments will be shown in previews"
}
]
},
%{
group: :pleroma,
key: :rich_media,
type: :group,
children: [
%{
key: :enabled,
type: :boolean,
description:
"if enabled the instance will parse metadata from attached links to generate link previews"
},
%{
key: :ignore_hosts,
type: {:list, :string},
description: "list of hosts which will be ignored by the metadata parser",
suggestions: [["accounts.google.com", "xss.website"]]
},
%{
key: :ignore_tld,
type: {:list, :string},
description: "list TLDs (top-level domains) which will ignore for parse metadata",
suggestions: [["local", "localdomain", "lan"]]
},
%{
key: :parsers,
type: {:list, :module},
description: "list of Rich Media parsers",
suggestions: [
Generator.richmedia_parsers()
]
},
%{
key: :ttl_setters,
type: {:list, :module},
description: "list of rich media ttl setters",
suggestions: [
[Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl]
]
}
]
},
%{
group: :pleroma,
key: :fetch_initial_posts,
type: :group,
description: "Fetching initial posts settings",
children: [
%{
key: :enabled,
type: :boolean,
description:
"if enabled, when a new user is federated with, fetch some of their latest posts"
},
%{
key: :pages,
type: :integer,
description: "the amount of pages to fetch",
suggestions: [5]
}
]
},
%{
group: :auto_linker,
key: :opts,
type: :group,
description: "Configuration for the auto_linker library",
children: [
%{
key: :class,
type: [:string, false],
description: "specify the class to be added to the generated link. false to clear",
suggestions: ["auto-linker", false]
},
%{
key: :rel,
type: [:string, false],
description: "override the rel attribute. false to clear",
suggestions: ["ugc", "noopener noreferrer", false]
},
%{
key: :new_window,
type: :boolean,
description: "set to false to remove target='_blank' attribute"
},
%{
key: :scheme,
type: :boolean,
description: "Set to true to link urls with schema http://google.com"
},
%{
key: :truncate,
type: [:integer, false],
description:
"Set to a number to truncate urls longer then the number. Truncated urls will end in `..`",
suggestions: [15, false]
},
%{
key: :strip_prefix,
type: :boolean,
description: "Strip the scheme prefix"
},
%{
key: :extra,
type: :boolean,
description: "link urls with rarely used schemes (magnet, ipfs, irc, etc.)"
}
]
},
%{
group: :pleroma,
key: Pleroma.ScheduledActivity,
type: :group,
description: "Scheduled activities settings",
children: [
%{
key: :daily_user_limit,
type: :integer,
description:
"the number of scheduled activities a user is allowed to create in a single day (Default: 25)",
suggestions: [25]
},
%{
key: :total_user_limit,
type: :integer,
description:
"the number of scheduled activities a user is allowed to create in total (Default: 300)",
suggestions: [300]
},
%{
key: :enabled,
type: :boolean,
description: "whether scheduled activities are sent to the job queue to be executed"
}
]
},
%{
group: :pleroma,
key: Pleroma.ActivityExpiration,
type: :group,
description: "Expired activity settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "whether expired activities will be sent to the job queue to be deleted"
}
]
},
%{
group: :pleroma,
type: :group,
description: "Authenticator",
children: [
%{
key: Pleroma.Web.Auth.Authenticator,
type: :module,
suggestions: [Pleroma.Web.Auth.PleromaAuthenticator, Pleroma.Web.Auth.LDAPAuthenticator]
}
]
},
%{
group: :pleroma,
key: :ldap,
type: :group,
description:
"Use LDAP for user authentication. When a user logs in to the Pleroma instance, the name and password" <>
" will be verified by trying to authenticate (bind) to an LDAP server." <>
" If a user exists in the LDAP directory but there is no account with the same name yet on the" <>
" Pleroma instance then a new Pleroma account will be created with the same name as the LDAP user name.",
children: [
%{
key: :enabled,
type: :boolean,
description: "enables LDAP authentication"
},
%{
key: :host,
type: :string,
description: "LDAP server hostname",
suggestions: ["localhosts"]
},
%{
key: :port,
type: :integer,
description: "LDAP port, e.g. 389 or 636",
suggestions: [389, 636]
},
%{
key: :ssl,
type: :boolean,
description: "true to use SSL, usually implies the port 636"
},
%{
key: :sslopts,
type: :keyword,
description: "additional SSL options"
},
%{
key: :tls,
type: :boolean,
description: "true to start TLS, usually implies the port 389"
},
%{
key: :tlsopts,
type: :keyword,
description: "additional TLS options"
},
%{
key: :base,
type: :string,
description: "LDAP base, e.g. \"dc=example,dc=com\"",
suggestions: ["dc=example,dc=com"]
},
%{
key: :uid,
type: :string,
description:
"LDAP attribute name to authenticate the user, e.g. when \"cn\", the filter will be \"cn=username,base\"",
suggestions: ["cn"]
}
]
},
%{
group: :pleroma,
key: :auth,
type: :group,
description: "Authentication / authorization settings",
children: [
%{
key: :auth_template,
type: :string,
description:
"authentication form template. By default it's show.html which corresponds to lib/pleroma/web/templates/o_auth/o_auth/show.html.ee",
suggestions: ["show.html"]
},
%{
key: :oauth_consumer_template,
type: :string,
description:
"OAuth consumer mode authentication form template. By default it's consumer.html which corresponds to" <>
" lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex",
suggestions: ["consumer.html"]
},
%{
key: :oauth_consumer_strategies,
type: :string,
description:
"the list of enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable." <>
" Each entry in this space-delimited string should be of format <strategy> or <strategy>:<dependency>" <>
" (e.g. twitter or keycloak:ueberauth_keycloak_strategy in case dependency is named differently than ueberauth_<strategy>).",
suggestions: ["twitter", "keycloak:ueberauth_keycloak_strategy"]
}
]
},
%{
group: :pleroma,
key: :email_notifications,
type: :group,
description: "Email notifications settings",
children: [
%{
key: :digest,
type: :map,
description:
"emails of \"what you've missed\" for users who have been inactive for a while",
suggestions: [
%{
active: false,
schedule: "0 0 * * 0",
interval: 7,
inactivity_threshold: 7
}
],
children: [
%{
key: :active,
type: :boolean,
description: "globally enable or disable digest emails"
},
%{
key: :schedule,
type: :string,
description:
"When to send digest email, in crontab format. \"0 0 0\" is the default, meaning \"once a week at midnight on Sunday morning\"",
suggestions: ["0 0 * * 0"]
},
%{
key: :interval,
type: :ininteger,
description: "Minimum interval between digest emails to one user",
suggestions: [7]
},
%{
key: :inactivity_threshold,
type: :integer,
description: "Minimum user inactivity threshold",
suggestions: [7]
}
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Emails.UserEmail,
type: :group,
description: "Email template settings",
children: [
%{
key: :logo,
type: [:string, nil],
description: "a path to a custom logo. Set it to nil to use the default Pleroma logo",
suggestions: ["some/path/logo.png", nil]
},
%{
key: :styling,
type: :map,
description: "a map with color settings for email templates.",
suggestions: [
%{
link_color: "#d8a070",
background_color: "#2C3645",
content_background_color: "#1B2635",
header_color: "#d8a070",
text_color: "#b9b9ba",
text_muted_color: "#b9b9ba"
}
],
children: [
%{
key: :link_color,
type: :string,
suggestions: ["#d8a070"]
},
%{
key: :background_color,
type: :string,
suggestions: ["#2C3645"]
},
%{
key: :content_background_color,
type: :string,
suggestions: ["#1B2635"]
},
%{
key: :header_color,
type: :string,
suggestions: ["#d8a070"]
},
%{
key: :text_color,
type: :string,
suggestions: ["#b9b9ba"]
},
%{
key: :text_muted_color,
type: :string,
suggestions: ["#b9b9ba"]
}
]
}
]
},
%{
group: :pleroma,
key: :oauth2,
type: :group,
description: "Configure OAuth 2 provider capabilities",
children: [
%{
key: :token_expires_in,
type: :integer,
description: "The lifetime in seconds of the access token",
suggestions: [600]
},
%{
key: :issue_new_refresh_token,
type: :boolean,
description:
"Keeps old refresh token or generate new refresh token when to obtain an access token"
},
%{
key: :clean_expired_tokens,
type: :boolean,
description: "Enable a background job to clean expired oauth tokens. Defaults to false"
},
%{
key: :clean_expired_tokens_interval,
type: :integer,
description:
"Interval to run the job to clean expired tokens. Defaults to 86_400_000 (24 hours).",
suggestions: [86_400_000]
}
]
},
%{
group: :pleroma,
key: :emoji,
type: :group,
children: [
%{
key: :shortcode_globs,
type: {:list, :string},
description: "Location of custom emoji files. * can be used as a wildcard",
suggestions: [["/emoji/custom/**/*.png"]]
},
%{
key: :pack_extensions,
type: {:list, :string},
description:
"A list of file extensions for emojis, when no emoji.txt for a pack is present",
suggestions: [[".png", ".gif"]]
},
%{
key: :groups,
type: :keyword,
description:
"Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname" <>
" and the value the location or array of locations. * can be used as a wildcard",
suggestions: [
[
# Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md`
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
]
]
},
%{
key: :default_manifest,
type: :string,
description:
"Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download." <>
" Currently only one manifest can be added (no arrays)",
suggestions: ["https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"]
},
%{
key: :shared_pack_cache_seconds_per_file,
type: :integer,
descpiption:
"When an emoji pack is shared, the archive is created and cached in memory" <>
" for this amount of seconds multiplied by the number of files.",
suggestions: [60]
}
]
},
%{
group: :pleroma,
key: :database,
type: :group,
description: "Database related settings",
children: [
%{
key: :rum_enabled,
type: :boolean,
description: "If RUM indexes should be used. Defaults to false"
}
]
},
%{
group: :pleroma,
key: :rate_limit,
type: :group,
description:
"Rate limit settings. This is an advanced feature enabled only for :authentication by default.",
children: [
%{
key: :search,
type: [:tuple, {:list, :tuple}],
description: "for the search requests (account & status search etc.)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :app_account_creation,
type: [:tuple, {:list, :tuple}],
description: "for registering user accounts from the same IP address",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :relations_actions,
type: [:tuple, {:list, :tuple}],
description: "for actions on relations with all users (follow, unfollow)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :relation_id_action,
type: [:tuple, {:list, :tuple}],
description: "for actions on relation with a specific user (follow, unfollow)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :statuses_actions,
type: [:tuple, {:list, :tuple}],
description:
"for create / delete / fav / unfav / reblog / unreblog actions on any statuses",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :status_id_action,
type: [:tuple, {:list, :tuple}],
description:
"for fav / unfav or reblog / unreblog actions on the same status by the same user",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
},
%{
key: :authentication,
type: [:tuple, {:list, :tuple}],
description: "for authentication create / password check / user existence check requests",
suggestions: [{60_000, 15}]
}
]
},
%{
group: :esshd,
type: :group,
description:
"To enable simple command line interface accessible over ssh, add a setting like this to your configuration file",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables ssh"
},
%{
key: :priv_dir,
type: :string,
description: "Dir with ssh keys",
suggestions: ["/some/path/ssh_keys"]
},
%{
key: :handler,
type: :string,
description: "Handler module",
suggestions: ["Pleroma.BBS.Handler"]
},
%{
key: :port,
type: :integer,
description: "Port to connect",
suggestions: [10_022]
},
%{
key: :password_authenticator,
type: :string,
description: "Authenticator module",
suggestions: ["Pleroma.BBS.Authenticator"]
}
]
},
%{
group: :mime,
type: :group,
description: "Mime types",
children: [
%{
key: :types,
type: :map,
suggestions: [
%{
"application/xml" => ["xml"],
"application/xrd+xml" => ["xrd+xml"],
"application/jrd+json" => ["jrd+json"],
"application/activity+json" => ["activity+json"],
"application/ld+json" => ["activity+json"]
}
],
children: [
%{
key: "application/xml",
type: {:list, :string},
suggestions: [["xml"]]
},
%{
key: "application/xrd+xml",
type: {:list, :string},
suggestions: [["xrd+xml"]]
},
%{
key: "application/jrd+json",
type: {:list, :string},
suggestions: [["jrd+json"]]
},
%{
key: "application/activity+json",
type: {:list, :string},
suggestions: [["activity+json"]]
},
%{
key: "application/ld+json",
type: {:list, :string},
suggestions: [["activity+json"]]
}
]
}
]
},
%{
group: :tesla,
type: :group,
description: "Tesla settings",
children: [
%{
key: :adapter,
type: :module,
description: "Tesla adapter",
suggestions: [Tesla.Adapter.Hackney]
}
]
},
%{
group: :pleroma,
key: :chat,
type: :group,
description: "Pleroma chat settings",
children: [
%{
key: :enabled,
type: :boolean
}
]
},
%{
group: :pleroma,
key: :suggestions,
type: :group,
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables suggestions"
},
%{
key: :third_party_engine,
type: :string,
description: "URL for third party engine",
suggestions: [
"http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-suggestions-api.cgi?{{host}}+{{user}}"
]
},
%{
key: :timeout,
type: :integer,
description: "Request timeout to third party engine",
suggestions: [300_000]
},
%{
key: :limit,
type: :integer,
description: "Limit for suggestions",
suggestions: [40]
},
%{
key: :web,
type: :string,
suggestions: ["https://vinayaka.distsn.org"]
}
]
},
%{
group: :prometheus,
key: Pleroma.Web.Endpoint.MetricsExporter,
type: :group,
description: "Prometheus settings",
children: [
%{
key: :path,
type: :string,
description: "API endpoint with metrics",
suggestions: ["/api/pleroma/app_metrics"]
}
]
},
%{
group: :http_signatures,
type: :group,
description: "HTTP Signatures settings",
children: [
%{
key: :adapter,
type: :module,
suggestions: [Pleroma.Signature]
}
]
},
%{
group: :pleroma,
key: Pleroma.Uploaders.MDII,
type: :group,
children: [
%{
key: :cgi,
type: :string,
suggestions: ["https://mdii.sakura.ne.jp/mdii-post.cgi"]
},
%{
key: :files,
type: :string,
suggestions: ["https://mdii.sakura.ne.jp"]
}
]
},
%{
group: :pleroma,
key: :http,
type: :group,
description: "HTTP settings",
children: [
%{
key: :proxy_url,
type: [:string, :atom, nil],
suggestions: ["localhost:9020", {:socks5, :localhost, 3090}, nil]
},
%{
key: :send_user_agent,
type: :boolean
},
%{
key: :adapter,
type: :keyword,
suggestions: [
[
ssl_options: [
# Workaround for remote server certificate chain issues
partial_chain: &:hackney_connect.partial_chain/1,
# We don't support TLS v1.3 yet
versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
]
]
]
}
]
},
%{
group: :pleroma,
key: :markup,
type: :group,
children: [
%{
key: :allow_inline_images,
type: :boolean
},
%{
key: :allow_headings,
type: :boolean
},
%{
key: :allow_tables,
type: :boolean
},
%{
key: :allow_fonts,
type: :boolean
},
%{
key: :scrub_policy,
type: {:list, :module},
suggestions: [[Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default]]
}
]
},
%{
group: :pleroma,
key: :user,
type: :group,
children: [
%{
key: :deny_follow_blocked,
type: :boolean
}
]
},
%{
group: :pleroma,
key: :mrf_normalize_markup,
type: :group,
children: [
%{
key: :scrub_policy,
type: :module,
suggestions: [Pleroma.HTML.Scrubber.Default]
}
]
},
%{
group: :pleroma,
key: Pleroma.User,
type: :group,
children: [
%{
key: :restricted_nicknames,
type: {:list, :string},
suggestions: [
[
".well-known",
"~",
"about",
"activities",
"api",
"auth",
"check_password",
"dev",
"friend-requests",
"inbox",
"internal",
"main",
"media",
"nodeinfo",
"notice",
"oauth",
"objects",
"ostatus_subscribe",
"pleroma",
"proxy",
"push",
"registration",
"relay",
"settings",
"status",
"tag",
"user-search",
"user_exists",
"users",
"web"
]
]
}
]
},
%{
group: :cors_plug,
type: :group,
children: [
%{
key: :max_age,
type: :integer,
suggestions: [86_400]
},
%{
key: :methods,
type: {:list, :string},
suggestions: [["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"]]
},
%{
key: :expose,
type: :string,
suggestions: [
[
"Link",
"X-RateLimit-Reset",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-Request-Id",
"Idempotency-Key"
]
]
},
%{
key: :credentials,
type: :boolean
},
%{
key: :headers,
type: {:list, :string},
suggestions: [["Authorization", "Content-Type", "Idempotency-Key"]]
}
]
},
%{
group: :pleroma,
key: Pleroma.Plugs.RemoteIp,
type: :group,
description: """
**If your instance is not behind at least one reverse proxy, you should not enable this plug.**
`Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
""",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enable/disable the plug. Defaults to `false`.",
suggestions: [true, false]
},
%{
key: :headers,
type: {:list, :string},
description:
"A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`."
},
%{
key: :proxies,
type: {:list, :string},
description:
"A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`."
},
%{
key: :reserved,
type: {:list, :string},
description:
"Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network)."
}
]
},
%{
group: :pleroma,
key: :web_cache_ttl,
type: :group,
description:
"The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration.",
children: [
%{
key: :activity_pub,
type: :integer,
description:
"activity pub routes (except question activities). Defaults to `nil` (no expiration).",
suggestions: [30_000, nil]
},
%{
key: :activity_pub_question,
type: :integer,
description:
"activity pub routes (question activities). Defaults to `30_000` (30 seconds).",
suggestions: [30_000]
}
]
}
]
diff --git a/config/releases.exs b/config/releases.exs
index 98c5ceccd..36c493673 100644
--- a/config/releases.exs
+++ b/config/releases.exs
@@ -1,19 +1,19 @@
import Config
-config :pleroma, :instance, static_dir: "/var/lib/pleroma/static"
+config :pleroma, :instance, static: "/var/lib/pleroma/static"
config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"
if File.exists?(config_path) do
import_config config_path
else
warning = [
IO.ANSI.red(),
IO.ANSI.bright(),
"!!! #{config_path} not found! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file",
IO.ANSI.reset()
]
IO.puts(warning)
end
diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index ee9e68cb1..e64ae6429 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -1,763 +1,838 @@
# Admin API
Authentication is required and the user must be an admin.
## `/api/pleroma/admin/users`
### List users
- Method `GET`
- Query Params:
- *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain)
- *optional* `filters`: **string** comma-separated string of filters:
- `local`: only local users
- `external`: only external users
- `active`: only active users
- `deactivated`: only deactivated users
- `is_admin`: users with admin role
- `is_moderator`: users with moderator role
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of users per page (default is `50`)
- *optional* `tags`: **[string]** tags list
- *optional* `name`: **string** user display name
- *optional* `email`: **string** user email
- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com`
- Response:
```json
{
"page_size": integer,
"count": integer,
"users": [
{
"deactivated": bool,
"id": integer,
"nickname": string,
"roles": {
"admin": bool,
"moderator": bool
},
"local": bool,
"tags": array,
"avatar": string,
"display_name": string
},
...
]
}
```
-## `/api/pleroma/admin/users`
+## DEPRECATED `DELETE /api/pleroma/admin/users`
### Remove a user
- Method `DELETE`
- Params:
- `nickname`
- Response: User’s nickname
+## `DELETE /api/pleroma/admin/users`
+
+### Remove a user
+
+- Method `DELETE`
+- Params:
+ - `nicknames`
+- Response: Array of user nicknames
+
### Create a user
- Method: `POST`
- Params:
`users`: [
{
`nickname`,
`email`,
`password`
}
]
- Response: User’s nickname
## `/api/pleroma/admin/users/follow`
### Make a user follow another user
- Methods: `POST`
- Params:
- `follower`: The nickname of the follower
- `followed`: The nickname of the followed
- Response:
- "ok"
## `/api/pleroma/admin/users/unfollow`
### Make a user unfollow another user
- Methods: `POST`
- Params:
- `follower`: The nickname of the follower
- `followed`: The nickname of the followed
- Response:
- "ok"
## `/api/pleroma/admin/users/:nickname/toggle_activation`
### Toggle user activation
- Method: `PATCH`
- Params:
- `nickname`
- Response: User’s object
```json
{
"deactivated": bool,
"id": integer,
"nickname": string
}
```
## `/api/pleroma/admin/users/tag`
### Tag a list of users
- Method: `PUT`
- Params:
- `nicknames` (array)
- `tags` (array)
### Untag a list of users
- Method: `DELETE`
- Params:
- `nicknames` (array)
- `tags` (array)
## `/api/pleroma/admin/users/:nickname/permission_group`
### Get user user permission groups membership
- Method: `GET`
- Params: none
- Response:
```json
{
"is_moderator": bool,
"is_admin": bool
}
```
## `/api/pleroma/admin/users/:nickname/permission_group/:permission_group`
Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesn’t exist.
### Get user user permission groups membership per permission group
- Method: `GET`
- Params: none
- Response:
```json
{
"is_moderator": bool,
"is_admin": bool
}
```
-### Add user in permission group
+## DEPRECATED `POST /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
+
+### Add user to permission group
-- Method: `POST`
- Params: none
- Response:
- On failure: `{"error": "…"}`
- - On success: JSON of the `user.info`
+ - On success: JSON of the user
+
+## `POST /api/pleroma/admin/users/permission_group/:permission_group`
+
+### Add users to permission group
+
+- Params:
+ - `nicknames`: nicknames array
+- Response:
+ - On failure: `{"error": "…"}`
+ - On success: JSON of the user
+
+## DEPRECATED `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
### Remove user from permission group
-- Method: `DELETE`
- Params: none
- Response:
- On failure: `{"error": "…"}`
- - On success: JSON of the `user.info`
+ - On success: JSON of the user
- Note: An admin cannot revoke their own admin status.
-## `/api/pleroma/admin/users/:nickname/activation_status`
+## `DELETE /api/pleroma/admin/users/permission_group/:permission_group`
+
+### Remove users from permission group
+
+- Params:
+ - `nicknames`: nicknames array
+- Response:
+ - On failure: `{"error": "…"}`
+ - On success: JSON of the user
+- Note: An admin cannot revoke their own admin status.
+
+## `PATCH /api/pleroma/admin/users/activate`
+
+### Activate user
+
+- Params:
+ - `nicknames`: nicknames array
+- Response:
+
+```json
+{
+ users: [
+ {
+ // user object
+ }
+ ]
+}
+```
+
+## `PATCH /api/pleroma/admin/users/deactivate`
+
+### Deactivate user
+
+- Params:
+ - `nicknames`: nicknames array
+- Response:
+
+```json
+{
+ users: [
+ {
+ // user object
+ }
+ ]
+}
+```
+
+## DEPRECATED `PATCH /api/pleroma/admin/users/:nickname/activation_status`
### Active or deactivate a user
-- Method: `PUT`
- Params:
- `nickname`
- `status` BOOLEAN field, false value means deactivation.
## `/api/pleroma/admin/users/:nickname_or_id`
### Retrive the details of a user
- Method: `GET`
- Params:
- `nickname` or `id`
- Response:
- On failure: `Not found`
- On success: JSON of the user
## `/api/pleroma/admin/users/:nickname_or_id/statuses`
### Retrive user's latest statuses
- Method: `GET`
- Params:
- `nickname` or `id`
- *optional* `page_size`: number of statuses to return (default is `20`)
- *optional* `godmode`: `true`/`false` – allows to see private statuses
- Response:
- On failure: `Not found`
- On success: JSON array of user's latest statuses
## `/api/pleroma/admin/relay`
### Follow a Relay
- Methods: `POST`
- Params:
- `relay_url`
- Response:
- On success: URL of the followed relay
### Unfollow a Relay
- Methods: `DELETE`
- Params:
- `relay_url`
- Response:
- On success: URL of the unfollowed relay
+## `GET /api/pleroma/admin/relay`
+
+### List Relays
+
+- Params: none
+- Response:
+ - On success: JSON array of relays
+
## `/api/pleroma/admin/users/invite_token`
### Create an account registration invite token
- Methods: `POST`
- Params:
- *optional* `max_use` (integer)
- *optional* `expires_at` (date string e.g. "2019-04-07")
- Response:
```json
{
"id": integer,
"token": string,
"used": boolean,
"expires_at": date,
"uses": integer,
"max_use": integer,
"invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)
}
```
## `/api/pleroma/admin/users/invites`
### Get a list of generated invites
- Methods: `GET`
- Params: none
- Response:
```json
{
"invites": [
{
"id": integer,
"token": string,
"used": boolean,
"expires_at": date,
"uses": integer,
"max_use": integer,
"invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)
},
...
]
}
```
## `/api/pleroma/admin/users/revoke_invite`
### Revoke invite by token
- Methods: `POST`
- Params:
- `token`
- Response:
```json
{
"id": integer,
"token": string,
"used": boolean,
"expires_at": date,
"uses": integer,
"max_use": integer,
"invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)
}
```
## `/api/pleroma/admin/users/email_invite`
### Sends registration invite via email
- Methods: `POST`
- Params:
- `email`
- `name`, optional
## `/api/pleroma/admin/users/:nickname/password_reset`
### Get a password reset token for a given nickname
- Methods: `GET`
- Params: none
- Response:
```json
{
"token": "base64 reset token",
"link": "https://pleroma.social/api/pleroma/password_reset/url-encoded-base64-token"
}
```
## `/api/pleroma/admin/users/:nickname/force_password_reset`
### Force passord reset for a user with a given nickname
- Methods: `PATCH`
- Params: none
- Response: none (code `204`)
## `/api/pleroma/admin/reports`
### Get a list of reports
- Method `GET`
- Params:
- *optional* `state`: **string** the state of reports. Valid values are `open`, `closed` and `resolved`
- *optional* `limit`: **integer** the number of records to retrieve
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
- Response:
- On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin
- On success: JSON, returns a list of reports, where:
- `account`: the user who has been reported
- `actor`: the user who has sent the report
- `statuses`: list of statuses that have been included to the report
```json
{
"total" : 1,
"reports": [
{
"account": {
"acct": "user",
"avatar": "https://pleroma.example.org/images/avi.png",
"avatar_static": "https://pleroma.example.org/images/avi.png",
"bot": false,
"created_at": "2019-04-23T17:32:04.000Z",
"display_name": "User",
"emojis": [],
"fields": [],
"followers_count": 1,
"following_count": 1,
"header": "https://pleroma.example.org/images/banner.png",
"header_static": "https://pleroma.example.org/images/banner.png",
"id": "9i6dAJqSGSKMzLG2Lo",
"locked": false,
"note": "",
"pleroma": {
"confirmation_pending": false,
"hide_favorites": true,
"hide_followers": false,
"hide_follows": false,
"is_admin": false,
"is_moderator": false,
"relationship": {},
"tags": []
},
"source": {
"note": "",
"pleroma": {},
"sensitive": false
},
"tags": ["force_unlisted"],
"statuses_count": 3,
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
"actor": {
"acct": "lain",
"avatar": "https://pleroma.example.org/images/avi.png",
"avatar_static": "https://pleroma.example.org/images/avi.png",
"bot": false,
"created_at": "2019-03-28T17:36:03.000Z",
"display_name": "Roger Braun",
"emojis": [],
"fields": [],
"followers_count": 1,
"following_count": 1,
"header": "https://pleroma.example.org/images/banner.png",
"header_static": "https://pleroma.example.org/images/banner.png",
"id": "9hEkA5JsvAdlSrocam",
"locked": false,
"note": "",
"pleroma": {
"confirmation_pending": false,
"hide_favorites": false,
"hide_followers": false,
"hide_follows": false,
"is_admin": false,
"is_moderator": false,
"relationship": {},
"tags": []
},
"source": {
"note": "",
"pleroma": {},
"sensitive": false
},
"tags": ["force_unlisted"],
"statuses_count": 1,
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
"content": "Please delete it",
"created_at": "2019-04-29T19:48:15.000Z",
"id": "9iJGOv1j8hxuw19bcm",
"state": "open",
"statuses": [
{
"account": { ... },
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<span class=\"h-card\"><a data-user=\"9hEkA5JsvAdlSrocam\" class=\"u-url mention\" href=\"https://pleroma.example.org/users/lain\">@<span>lain</span></a></span> click on my link <a href=\"https://www.google.com/\">https://www.google.com/</a>",
"created_at": "2019-04-23T19:15:47.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9i6mQ9uVrrOmOime8m",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "lain",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "@lain click on my link https://www.google.com/"
},
"conversation_id": 28,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/8717b90f-8e09-4b58-97b0-e3305472b396",
"url": "https://pleroma.example.org/notice/9i6mQ9uVrrOmOime8m",
"visibility": "direct"
}
]
}
]
}
```
## `/api/pleroma/admin/reports/:id`
### Get an individual report
- Method `GET`
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id`
### Change the state of the report
- Method `PUT`
- Params:
- `id`
- `state`: required, the new state. Valid values are `open`, `closed` and `resolved`
- Response:
- On failure:
- 400 Bad Request `"Unsupported state"`
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id/respond`
### Respond to a report
- Method `POST`
- Params:
- `id`
- `status`: required, the message
- Response:
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, created Mastodon Status entity
```json
{
"account": { ... },
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "Your claim is going to be closed",
"created_at": "2019-05-11T17:13:03.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9ihuiSL1405I65TmEq",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
{
"acct": "admin",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/admin",
"username": "admin"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "Your claim is going to be closed"
},
"conversation_id": 35,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/cab0836d-9814-46cd-a0ea-529da9db5fcb",
"url": "https://pleroma.example.org/notice/9ihuiSL1405I65TmEq",
"visibility": "direct"
}
```
## `/api/pleroma/admin/statuses/:id`
### Change the scope of an individual reported status
- Method `PUT`
- Params:
- `id`
- `sensitive`: optional, valid values are `true` or `false`
- `visibility`: optional, valid values are `public`, `private` and `unlisted`
- Response:
- On failure:
- 400 Bad Request `"Unsupported visibility"`
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Mastodon Status entity
## `/api/pleroma/admin/statuses/:id`
### Delete an individual reported status
- Method `DELETE`
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: 200 OK `{}`
## `/api/pleroma/admin/config/migrate_to_db`
### Run mix task pleroma.config migrate_to_db
Copy settings on key `:pleroma` to DB.
- Method `GET`
- Params: none
- Response:
```json
{}
```
## `/api/pleroma/admin/config/migrate_from_db`
### Run mix task pleroma.config migrate_from_db
Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB.
- Method `GET`
- Params: none
- Response:
```json
{}
```
## `/api/pleroma/admin/config`
### List config settings
List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
- Method `GET`
- Params: none
- Response:
```json
{
configs: [
{
"group": string,
"key": string or string with leading `:` for atoms,
"value": string or {} or [] or {"tuple": []}
}
]
}
```
## `/api/pleroma/admin/config`
### Update config settings
Updating config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`.
Module name can be passed as string, which starts with `Pleroma`, e.g. `"Pleroma.Upload"`.
Atom keys and values can be passed with `:` in the beginning, e.g. `":upload"`.
Tuples can be passed as `{"tuple": ["first_val", Pleroma.Module, []]}`.
`{"tuple": ["some_string", "Pleroma.Some.Module", []]}` will be converted to `{"some_string", Pleroma.Some.Module, []}`.
Keywords can be passed as lists with 2 child tuples, e.g.
`[{"tuple": ["first_val", Pleroma.Module]}, {"tuple": ["second_val", true]}]`.
If value contains list of settings `[subkey: val1, subkey2: val2, subkey3: val3]`, it's possible to remove only subkeys instead of all settings passing `subkeys` parameter. E.g.:
{"group": "pleroma", "key": "some_key", "delete": "true", "subkeys": [":subkey", ":subkey3"]}.
Compile time settings (need instance reboot):
- all settings by this keys:
- `:hackney_pools`
- `:chat`
- `Pleroma.Web.Endpoint`
- `Pleroma.Repo`
- part settings:
- `Pleroma.Captcha` -> `:seconds_valid`
- `Pleroma.Upload` -> `:proxy_remote`
- `:instance` -> `:upload_limit`
- Method `POST`
- Params:
- `configs` => [
- `group` (string)
- `key` (string or string with leading `:` for atoms)
- `value` (string, [], {} or {"tuple": []})
- `delete` = true (optional, if parameter must be deleted)
- `subkeys` [(string with leading `:` for atoms)] (optional, works only if `delete=true` parameter is passed, otherwise will be ignored)
]
- Request (example):
```json
{
configs: [
{
"group": "pleroma",
"key": "Pleroma.Upload",
"value": [
{"tuple": [":uploader", "Pleroma.Uploaders.Local"]},
{"tuple": [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
{"tuple": [":link_name", true]},
{"tuple": [":proxy_remote", false]},
{"tuple": [":proxy_opts", [
{"tuple": [":redirect_on_failure", false]},
{"tuple": [":max_body_length", 1048576]},
{"tuple": [":http": [
{"tuple": [":follow_redirect", true]},
{"tuple": [":pool", ":upload"]},
]]}
]
]},
{"tuple": [":dispatch", {
"tuple": ["/api/v1/streaming", "Pleroma.Web.MastodonAPI.WebsocketHandler", []]
}]}
]
}
]
}
```
- Response:
```json
{
configs: [
{
"group": string,
"key": string or string with leading `:` for atoms,
"value": string or {} or [] or {"tuple": []}
}
]
}
```
## `/api/pleroma/admin/moderation_log`
### Get moderation log
- Method `GET`
- Params:
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
- *optional* `start_date`: **datetime (ISO 8601)** filter logs by creation date, start from `start_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. `2005-08-09T18:31:42`
- *optional* `end_date`: **datetime (ISO 8601)** filter logs by creation date, end by from `end_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. 2005-08-09T18:31:42
- *optional* `user_id`: **integer** filter logs by actor's id
- *optional* `search`: **string** search logs by the log message
- Response:
```json
[
{
"data": {
"actor": {
"id": 1,
"nickname": "lain"
},
"action": "relay_follow"
},
"time": 1502812026, // timestamp
"message": "[2017-08-15 15:47:06] @nick0 followed relay: https://example.org/relay" // log message
}
]
```
## `POST /api/pleroma/admin/reload_emoji`
### Reload the instance's custom emoji
* Method `POST`
* Authentication: required
* Params: None
* Response: JSON, "ok" and 200 status
diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md
index 0517bbdd7..6c326dc9b 100644
--- a/docs/API/pleroma_api.md
+++ b/docs/API/pleroma_api.md
@@ -1,474 +1,481 @@
# Pleroma API
Requests that require it can be authenticated with [an OAuth token](https://tools.ietf.org/html/rfc6749), the `_pleroma_key` cookie, or [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization).
Request parameters can be passed via [query strings](https://en.wikipedia.org/wiki/Query_string) or as [form data](https://www.w3.org/TR/html401/interact/forms.html). Files must be uploaded as `multipart/form-data`.
## `/api/pleroma/emoji`
### Lists the custom emoji on that server.
* Method: `GET`
* Authentication: not required
* Params: none
* Response: JSON
* Example response:
```json
{
"girlpower": {
"tags": [
"Finmoji"
],
"image_url": "/finmoji/128px/girlpower-128.png"
},
"education": {
"tags": [
"Finmoji"
],
"image_url": "/finmoji/128px/education-128.png"
},
"finnishlove": {
"tags": [
"Finmoji"
],
"image_url": "/finmoji/128px/finnishlove-128.png"
}
}
```
* Note: Same data as Mastodon API’s `/api/v1/custom_emojis` but in a different format
## `/api/pleroma/follow_import`
### Imports your follows, for example from a Mastodon CSV file.
* Method: `POST`
* Authentication: required
* Params:
* `list`: STRING or FILE containing a whitespace-separated list of accounts to follow
* Response: HTTP 200 on success, 500 on error
* Note: Users that can't be followed are silently skipped.
## `/api/pleroma/captcha`
### Get a new captcha
* Method: `GET`
* Authentication: not required
* Params: none
* Response: Provider specific JSON, the only guaranteed parameter is `type`
* Example response: `{"type": "kocaptcha", "token": "whatever", "url": "https://captcha.kotobank.ch/endpoint"}`
## `/api/pleroma/delete_account`
### Delete an account
* Method `POST`
* Authentication: required
* Params:
* `password`: user's password
* Response: JSON. Returns `{"status": "success"}` if the deletion was successful, `{"error": "[error message]"}` otherwise
* Example response: `{"error": "Invalid password."}`
## `/api/pleroma/disable_account`
### Disable an account
* Method `POST`
* Authentication: required
* Params:
* `password`: user's password
* Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise
* Example response: `{"error": "Invalid password."}`
## `/api/account/register`
### Register a new user
* Method `POST`
* Authentication: not required
* Params:
* `nickname`
* `fullname`
* `bio`
* `email`
* `password`
* `confirm`
* `captcha_solution`: optional, contains provider-specific captcha solution,
* `captcha_token`: optional, contains provider-specific captcha token
* `token`: invite token required when the registrations aren't public.
* Response: JSON. Returns a user object on success, otherwise returns `{"error": "error_msg"}`
* Example response:
```json
{
"background_image": null,
"cover_photo": "https://pleroma.soykaf.com/images/banner.png",
"created_at": "Tue Dec 18 16:55:56 +0000 2018",
"default_scope": "public",
"description": "blushy-crushy fediverse idol + pleroma dev\nlet's be friends \nぷれろまの生徒会長。謎の外人。日本語OK. \n公主病.",
"description_html": "blushy-crushy fediverse idol + pleroma dev.<br />let's be friends <br />ぷれろまの生徒会長。謎の外人。日本語OK. <br />公主病.",
"favourites_count": 0,
"fields": [],
"followers_count": 0,
"following": false,
"follows_you": false,
"friends_count": 0,
"id": 6,
"is_local": true,
"locked": false,
"name": "lain",
"name_html": "lain",
"no_rich_text": false,
"pleroma": {
"tags": []
},
"profile_image_url": "https://pleroma.soykaf.com/images/avi.png",
"profile_image_url_https": "https://pleroma.soykaf.com/images/avi.png",
"profile_image_url_original": "https://pleroma.soykaf.com/images/avi.png",
"profile_image_url_profile_size": "https://pleroma.soykaf.com/images/avi.png",
"rights": {
"delete_others_notice": false
},
"screen_name": "lain",
"statuses_count": 0,
"statusnet_blocking": false,
"statusnet_profile_url": "https://pleroma.soykaf.com/users/lain"
}
```
## `/api/pleroma/admin/`…
See [Admin-API](admin_api.md)
## `/api/v1/pleroma/notifications/read`
### Mark notifications as read
* Method `POST`
* Authentication: required
* Params (mutually exclusive):
* `id`: a single notification id to read
* `max_id`: read all notifications up to this id
* Response: Notification entity/Array of Notification entities that were read. In case of `max_id`, only the first 80 read notifications will be returned.
## `/api/v1/pleroma/accounts/:id/subscribe`
### Subscribe to receive notifications for all statuses posted by a user
* Method `POST`
* Authentication: required
* Params:
* `id`: account id to subscribe to
* Response: JSON, returns a mastodon relationship object on success, otherwise returns `{"error": "error_msg"}`
* Example response:
```json
{
"id": "abcdefg",
"following": true,
"followed_by": false,
"blocking": false,
"muting": false,
"muting_notifications": false,
"subscribing": true,
"requested": false,
"domain_blocking": false,
"showing_reblogs": true,
"endorsed": false
}
```
## `/api/v1/pleroma/accounts/:id/unsubscribe`
### Unsubscribe to stop receiving notifications from user statuses
* Method `POST`
* Authentication: required
* Params:
* `id`: account id to unsubscribe from
* Response: JSON, returns a mastodon relationship object on success, otherwise returns `{"error": "error_msg"}`
* Example response:
```json
{
"id": "abcdefg",
"following": true,
"followed_by": false,
"blocking": false,
"muting": false,
"muting_notifications": false,
"subscribing": false,
"requested": false,
"domain_blocking": false,
"showing_reblogs": true,
"endorsed": false
}
```
## `/api/v1/pleroma/accounts/:id/favourites`
### Returns favorites timeline of any user
* Method `GET`
* Authentication: not required
* Params:
* `id`: the id of the account for whom to return results
* `limit`: optional, the number of records to retrieve
* `since_id`: optional, returns results that are more recent than the specified id
* `max_id`: optional, returns results that are older than the specified id
* Response: JSON, returns a list of Mastodon Status entities on success, otherwise returns `{"error": "error_msg"}`
* Example response:
```json
[
{
"account": {
"id": "9hptFmUF3ztxYh3Svg",
"url": "https://pleroma.example.org/users/nick2",
"username": "nick2",
...
},
"application": {"name": "Web", "website": null},
"bookmarked": false,
"card": null,
"content": "This is :moominmamma: note 0",
"created_at": "2019-04-15T15:42:15.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 1,
"id": "9hptFmVJ02khbzYJaS",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [],
"muted": false,
"pinned": false,
"pleroma": {
"content": {"text/plain": "This is :moominmamma: note 0"},
"conversation_id": 13679,
"local": true,
"spoiler_text": {"text/plain": "2hu"}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "2hu",
"tags": [{"name": "2hu", "url": "/tag/2hu"}],
"uri": "https://pleroma.example.org/objects/198ed2a1-7912-4482-b559-244a0369e984",
"url": "https://pleroma.example.org/notice/9hptFmVJ02khbzYJaS",
"visibility": "public"
}
]
```
## `/api/v1/pleroma/accounts/update_*`
### Set and clear account avatar, banner, and background
- PATCH `/api/v1/pleroma/accounts/update_avatar`: Set/clear user avatar image
- PATCH `/api/v1/pleroma/accounts/update_banner`: Set/clear user banner image
- PATCH `/api/v1/pleroma/accounts/update_background`: Set/clear user background image
## `/api/v1/pleroma/accounts/confirmation_resend`
### Resend confirmation email
* Method `POST`
* Params:
* `email`: email of that needs to be verified
* Authentication: not required
* Response: 204 No Content
## `/api/v1/pleroma/mascot`
### Gets user mascot image
* Method `GET`
* Authentication: required
* Response: JSON. Returns a mastodon media attachment entity.
* Example response:
```json
{
"id": "abcdefg",
"url": "https://pleroma.example.org/media/abcdefg.png",
"type": "image",
"pleroma": {
"mime_type": "image/png"
}
}
```
### Updates user mascot image
* Method `PUT`
* Authentication: required
* Params:
* `image`: Multipart image
* Response: JSON. Returns a mastodon media attachment entity
when successful, otherwise returns HTTP 415 `{"error": "error_msg"}`
* Example response:
```json
{
"id": "abcdefg",
"url": "https://pleroma.example.org/media/abcdefg.png",
"type": "image",
"pleroma": {
"mime_type": "image/png"
}
}
```
* Note: Behaves exactly the same as `POST /api/v1/upload`.
Can only accept images - any attempt to upload non-image files will be met with `HTTP 415 Unsupported Media Type`.
## `/api/pleroma/notification_settings`
### Updates user notification settings
* Method `PUT`
* Authentication: required
* Params:
* `followers`: BOOLEAN field, receives notifications from followers
* `follows`: BOOLEAN field, receives notifications from people the user follows
* `remote`: BOOLEAN field, receives notifications from people on remote instances
* `local`: BOOLEAN field, receives notifications from people on the local instance
* Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}`
## `/api/pleroma/healthcheck`
### Healthcheck endpoint with additional system data.
* Method `GET`
* Authentication: not required
* Params: none
* Response: JSON, statuses (200 - healthy, 503 unhealthy).
* Example response:
```json
{
"pool_size": 0, # database connection pool
"active": 0, # active processes
"idle": 0, # idle processes
"memory_used": 0.00, # Memory used
"healthy": true, # Instance state
"job_queue_stats": {} # Job queue stats
}
```
## `/api/pleroma/change_email`
### Change account email
* Method `POST`
* Authentication: required
* Params:
* `password`: user's password
* `email`: new email
* Response: JSON. Returns `{"status": "success"}` if the change was successful, `{"error": "[error message]"}` otherwise
* Note: Currently, Mastodon has no API for changing email. If they add it in future it might be incompatible with Pleroma.
# Pleroma Conversations
Pleroma Conversations have the same general structure that Mastodon Conversations have. The behavior differs in the following ways when using these endpoints:
1. Pleroma Conversations never add or remove recipients, unless explicitly changed by the user.
2. Pleroma Conversations statuses can be requested by Conversation id.
3. Pleroma Conversations can be replied to.
Conversations have the additional field "recipients" under the "pleroma" key. This holds a list of all the accounts that will receive a message in this conversation.
The status posting endpoint takes an additional parameter, `in_reply_to_conversation_id`, which, when set, will set the visiblity to direct and address only the people who are the recipients of that Conversation.
## `GET /api/v1/pleroma/conversations/:id/statuses`
### Timeline for a given conversation
* Method `GET`
* Authentication: required
* Params: Like other timelines
* Response: JSON, statuses (200 - healthy, 503 unhealthy).
## `GET /api/v1/pleroma/conversations/:id`
### The conversation with the given ID.
* Method `GET`
* Authentication: required
* Params: None
* Response: JSON, statuses (200 - healthy, 503 unhealthy).
## `PATCH /api/v1/pleroma/conversations/:id`
### Update a conversation. Used to change the set of recipients.
* Method `PATCH`
* Authentication: required
* Params:
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
+## `GET /api/v1/pleroma/conversations/read`
+### Marks all user's conversations as read.
+* Method `POST`
+* Authentication: required
+* Params: None
+* Response: JSON, returns a list of Mastodon Conversation entities that were marked as read (200 - healthy, 503 unhealthy).
+
## `GET /api/pleroma/emoji/packs`
### Lists the custom emoji packs on the server
* Method `GET`
* Authentication: not required
* Params: None
* Response: JSON, "ok" and 200 status and the JSON hashmap of "pack name" to "pack contents"
## `PUT /api/pleroma/emoji/packs/:name`
### Creates an empty custom emoji pack
* Method `PUT`
* Authentication: required
* Params: None
* Response: JSON, "ok" and 200 status or 409 if the pack with that name already exists
## `DELETE /api/pleroma/emoji/packs/:name`
### Delete a custom emoji pack
* Method `DELETE`
* Authentication: required
* Params: None
* Response: JSON, "ok" and 200 status or 500 if there was an error deleting the pack
## `POST /api/pleroma/emoji/packs/:name/update_file`
### Update a file in a custom emoji pack
* Method `POST`
* Authentication: required
* Params:
* if the `action` is `add`, adds an emoji named `shortcode` to the pack `pack_name`,
that means that the emoji file needs to be uploaded with the request
(thus requiring it to be a multipart request) and be named `file`.
There can also be an optional `filename` that will be the new emoji file name
(if it's not there, the name will be taken from the uploaded file).
* if the `action` is `update`, changes emoji shortcode
(from `shortcode` to `new_shortcode` or moves the file (from the current filename to `new_filename`)
* if the `action` is `remove`, removes the emoji named `shortcode` and it's associated file
* Response: JSON, updated "files" section of the pack and 200 status, 409 if the trying to use a shortcode
that is already taken, 400 if there was an error with the shortcode, filename or file (additional info
in the "error" part of the response JSON)
## `POST /api/pleroma/emoji/packs/:name/update_metadata`
### Updates (replaces) pack metadata
* Method `POST`
* Authentication: required
* Params:
* `new_data`: new metadata to replace the old one
* Response: JSON, updated "metadata" section of the pack and 200 status or 400 if there was a
problem with the new metadata (the error is specified in the "error" part of the response JSON)
## `POST /api/pleroma/emoji/packs/download_from`
### Requests the instance to download the pack from another instance
* Method `POST`
* Authentication: required
* Params:
* `instance_address`: the address of the instance to download from
* `pack_name`: the pack to download from that instance
* Response: JSON, "ok" and 200 status if the pack was downloaded, or 500 if there were
errors downloading the pack
## `POST /api/pleroma/emoji/packs/list_from`
### Requests the instance to list the packs from another instance
* Method `POST`
* Authentication: required
* Params:
* `instance_address`: the address of the instance to download from
* Response: JSON with the pack list, same as if the request was made to that instance's
list endpoint directly + 200 status
## `GET /api/pleroma/emoji/packs/:name/download_shared`
### Requests a local pack from the instance
* Method `GET`
* Authentication: not required
* Params: None
* Response: the archive of the pack with a 200 status code, 403 if the pack is not set as shared,
404 if the pack does not exist
## `GET /api/v1/pleroma/accounts/:id/scrobbles`
### Requests a list of current and recent Listen activities for an account
* Method `GET`
* Authentication: not required
* Params: None
* Response: An array of media metadata entities.
* Example response:
```json
[
{
"account": {...},
"id": "1234",
"title": "Some Title",
"artist": "Some Artist",
"album": "Some Album",
"length": 180000,
"created_at": "2019-09-28T12:40:45.000Z"
}
]
```
## `POST /api/v1/pleroma/scrobble`
### Creates a new Listen activity for an account
* Method `POST`
* Authentication: required
* Params:
* `title`: the title of the media playing
* `album`: the album of the media playing [optional]
* `artist`: the artist of the media playing [optional]
* `length`: the length of the media playing [optional]
* Response: the newly created media metadata entity representing the Listen activity
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 8d85276ed..3427ae419 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -1,777 +1,806 @@
# Configuration Cheat Sheet
This is a cheat sheet for Pleroma configuration file, any setting possible to configure should be listed here.
Pleroma configuration works by first importing the base config (`config/config.exs` on source installs, compiled-in on OTP releases), then overriding it by the environment config (`config/$MIX_ENV.exs` on source installs, N/A to OTP releases) and then overriding it by user config (`config/$MIX_ENV.secret.exs` on source installs, typically `/etc/pleroma/config.exs` on OTP releases).
You shouldn't edit the base config directly to avoid breakages and merge conflicts, but it can be used as a reference if you don't understand how an option is supposed to be formatted, the latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs).
## Pleroma.Upload
* `uploader`: Select which `Pleroma.Uploaders` to use
* `filters`: List of `Pleroma.Upload.Filter` to use.
* `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe`
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
!!! warning
`strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
## Pleroma.Uploaders.Local
* `uploads`: Which directory to store the user-uploads in, relative to pleroma’s working directory
## Pleroma.Uploaders.S3
* `bucket`: S3 bucket name
* `bucket_namespace`: S3 bucket namespace
* `public_endpoint`: S3 endpoint that the user finally accesses(ex. "https://s3.dualstack.ap-northeast-1.amazonaws.com")
* `truncated_namespace`: If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or "" etc.
For example, when using CDN to S3 virtual host format, set "".
At this time, write CNAME to CDN in public_endpoint.
* `streaming_enabled`: Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems.
## Pleroma.Upload.Filter.Mogrify
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`.
## Pleroma.Upload.Filter.Dedupe
No specific configuration.
## Pleroma.Upload.Filter.AnonymizeFilename
This filter replaces the filename (not the path) of an upload. For complete obfuscation, add
`Pleroma.Upload.Filter.Dedupe` before AnonymizeFilename.
* `text`: Text to replace filenames in links. If empty, `{random}.extension` will be used. You can get the original filename extension by using `{extension}`, for example `custom-file-name.{extension}`.
## Pleroma.Emails.Mailer
* `adapter`: one of the mail adapters listed in [Swoosh readme](https://github.com/swoosh/swoosh#adapters), or `Swoosh.Adapters.Local` for in-memory mailbox.
* `api_key` / `password` and / or other adapter-specific settings, per the above documentation.
* `enabled`: Allows enable/disable send emails. Default: `false`.
An example for Sendgrid adapter:
```elixir
config :pleroma, Pleroma.Emails.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: "YOUR_API_KEY"
```
An example for SMTP adapter:
```elixir
config :pleroma, Pleroma.Emails.Mailer,
adapter: Swoosh.Adapters.SMTP,
relay: "smtp.gmail.com",
username: "YOUR_USERNAME@gmail.com",
password: "YOUR_SMTP_PASSWORD",
port: 465,
ssl: true,
tls: :always,
auth: :always
```
## :uri_schemes
* `valid_schemes`: List of the scheme part that is considered valid to be an URL
## :instance
* `name`: The instance’s name
* `email`: Email used to reach an Administrator/Moderator of the instance
* `notify_email`: Email used for notifications.
* `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``
* `limit`: Posts character limit (CW/Subject included in the counter)
* `remote_limit`: Hard character limit beyond which remote posts will be dropped.
* `upload_limit`: File size limit of uploads (except for avatar, background, banner)
* `avatar_upload_limit`: File size limit of user’s profile avatars
* `background_upload_limit`: File size limit of user’s profile backgrounds
* `banner_upload_limit`: File size limit of user’s profile banners
* `poll_limits`: A map with poll limits for **local** polls
* `max_options`: Maximum number of options
* `max_option_chars`: Maximum number of characters per option
* `min_expiration`: Minimum expiration time (in seconds)
* `max_expiration`: Maximum expiration time (in seconds)
* `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
* `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
* `account_activation_required`: Require users to confirm their emails before signing in.
* `federating`: Enable federation with other instances
* `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
* `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
* `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance
* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default)
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See ``:mrf_simple`` section)
* `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive)
* `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (see ``:mrf_subchain`` section)
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See ``:mrf_rejectnonpublic`` section)
* `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
* `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links.
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed.
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (see `:mrf_mention` section)
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (see `:mrf_vocabulary` section)
* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
* `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json``
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML)
* `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `mrf_transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
* `extended_nickname_format`: Set to `true` to use extended local nicknames format (allows underscores/dashes). This will break federation with
older software for theses nicknames.
* `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature.
* `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow.
* `no_attachment_links`: Set to true to disable automatically adding attachment link text to statuses
* `welcome_message`: A message that will be send to a newly registered users as a direct message.
* `welcome_user_nickname`: The nickname of the local user that sends the welcome message.
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`)
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). Default: `false`.
* `healthcheck`: If set to true, system data will be shown on ``/api/pleroma/healthcheck``.
* `remote_post_retention_days`: The default amount of days to retain remote posts when pruning the database.
* `user_bio_length`: A user bio maximum length (default: `5000`)
* `user_name_length`: A user name maximum length (default: `100`)
* `skip_thread_containment`: Skip filter out broken threads. The default is `false`.
* `limit_to_local_content`: Limit unauthenticated users to search for local statutes and users only. Possible values: `:unauthenticated`, `:all` and `false`. The default is `:unauthenticated`.
* `max_account_fields`: The maximum number of custom fields in the user profile (default: `10`)
* `max_remote_account_fields`: The maximum number of custom fields in the remote user profile (default: `20`)
* `account_field_name_length`: An account field name maximum length (default: `512`)
* `account_field_value_length`: An account field value maximum length (default: `2048`)
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
!!! danger
This is a Work In Progress, not usable just yet
* `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api.
## :logger
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack
An example to enable ONLY ExSyslogger (f/ex in ``prod.secret.exs``) with info and debug suppressed:
```elixir
config :logger,
backends: [{ExSyslogger, :ex_syslogger}]
config :logger, :ex_syslogger,
level: :warn
```
Another example, keeping console output and adding the pid to syslog output:
```elixir
config :logger,
backends: [:console, {ExSyslogger, :ex_syslogger}]
config :logger, :ex_syslogger,
level: :warn,
option: [:pid, :ndelay]
```
See: [logger’s documentation](https://hexdocs.pm/logger/Logger.html) and [ex_syslogger’s documentation](https://hexdocs.pm/ex_syslogger/)
An example of logging info to local syslog, but warn to a Slack channel:
```elixir
config :logger,
backends: [ {ExSyslogger, :ex_syslogger}, Quack.Logger ],
level: :info
config :logger, :ex_syslogger,
level: :info,
ident: "pleroma",
format: "$metadata[$level] $message"
config :quack,
level: :warn,
meta: [:all],
webhook_url: "https://hooks.slack.com/services/YOUR-API-KEY-HERE"
```
See the [Quack Github](https://github.com/azohra/quack) for more details
## :frontend_configurations
This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` and `masto_fe` are configured. You can find the documentation for `pleroma_fe` configuration into [Pleroma-FE configuration and customization for instance administrators](/frontend/CONFIGURATION/#options).
Frontends can access these settings at `/api/pleroma/frontend_configurations`
To add your own configuration for PleromaFE, use it like this:
```elixir
config :pleroma, :frontend_configurations,
pleroma_fe: %{
theme: "pleroma-dark",
# ... see /priv/static/static/config.json for the available keys.
},
masto_fe: %{
showInstanceSpecificPanel: true
}
```
These settings **need to be complete**, they will override the defaults.
NOTE: for versions < 1.0, you need to set [`:fe`](#fe) to false, as shown a few lines below.
## :fe
!!! warning
__THIS IS DEPRECATED__
If you are using this method, please change it to the [`frontend_configurations`](#frontend_configurations) method.
Please **set this option to false** in your config like this:
```elixir
config :pleroma, :fe, false
```
This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false.
* `theme`: Which theme to use, they are defined in ``styles.json``
* `logo`: URL of the logo, defaults to Pleroma’s logo
* `logo_mask`: Whether to use only the logo's shape as a mask (true) or as a regular image (false)
* `logo_margin`: What margin to use around the logo
* `background`: URL of the background, unless viewing a user profile with a background that is set
* `redirect_root_no_login`: relative URL which indicates where to redirect when a user isn’t logged in.
* `redirect_root_login`: relative URL which indicates where to redirect when a user is logged in.
* `show_instance_panel`: Whenether to show the instance’s specific panel.
* `scope_options_enabled`: Enable setting an notice visibility and subject/CW when posting
* `formatting_options_enabled`: Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to ``:instance, allowed_post_formats``
* `collapse_message_with_subjects`: When a message has a subject(aka Content Warning), collapse it by default
* `hide_post_stats`: Hide notices statistics(repeats, favorites, …)
* `hide_user_stats`: Hide profile statistics(posts, posts per day, followers, followings, …)
## :assets
This section configures assets to be used with various frontends. Currently the only option
relates to mascots on the mastodon frontend
* `mascots`: KeywordList of mascots, each element __MUST__ contain both a `url` and a
`mime_type` key.
* `default_mascot`: An element from `mascots` - This will be used as the default mascot
on MastoFE (default: `:pleroma_fox_tan`)
+## :manifest
+
+This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE.
+
+* `icons`: Describe the icons of the app, this a list of maps describing icons in the same way as the
+ [spec](https://www.w3.org/TR/appmanifest/#imageresource-and-its-members) describes it.
+
+ Example:
+
+ ```elixir
+ config :pleroma, :manifest,
+ icons: [
+ %{
+ src: "/static/logo.png"
+ },
+ %{
+ src: "/static/icon.png",
+ type: "image/png"
+ },
+ %{
+ src: "/static/icon.ico",
+ sizes: "72x72 96x96 128x128 256x256"
+ }
+ ]
+ ```
+
+* `theme_color`: Describe the theme color of the app. (Example: `"#282c37"`, `"rebeccapurple"`)
+* `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`)
+
## :mrf_simple
* `media_removal`: List of instances to remove medias from
* `media_nsfw`: List of instances to put medias as NSFW(sensitive) from
* `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline
* `reject`: List of instances to reject any activities from
* `accept`: List of instances to accept any activities from
* `report_removal`: List of instances to reject reports from
* `avatar_removal`: List of instances to strip avatars from
* `banner_removal`: List of instances to strip banners from
## :mrf_subchain
This policy processes messages through an alternate pipeline when a given message matches certain criteria.
All criteria are configured as a map of regular expressions to lists of policy modules.
* `match_actor`: Matches a series of regular expressions against the actor field.
Example:
```elixir
config :pleroma, :mrf_subchain,
match_actor: %{
~r/https:\/\/example.com/s => [Pleroma.Web.ActivityPub.MRF.DropPolicy]
}
```
## :mrf_rejectnonpublic
* `allow_followersonly`: whether to allow followers-only posts
* `allow_direct`: whether to allow direct messages
## :mrf_hellthread
* `delist_threshold`: Number of mentioned users after which the message gets delisted (the message can still be seen, but it will not show up in public timelines and mentioned users won't get notifications about it). Set to 0 to disable.
* `reject_threshold`: Number of mentioned users after which the messaged gets rejected. Set to 0 to disable.
## :mrf_keyword
* `reject`: A list of patterns which result in message being rejected, each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html)
* `federated_timeline_removal`: A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html)
* `replace`: A list of tuples containing `{pattern, replacement}`, `pattern` can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html)
## :mrf_mention
* `actors`: A list of actors, for which to drop any posts mentioning.
## :mrf_vocabulary
* `accept`: A list of ActivityStreams terms to accept. If empty, all supported messages are accepted.
* `reject`: A list of ActivityStreams terms to reject. If empty, no messages are rejected.
## :media_proxy
* `enabled`: Enables proxying of remote media to the instance’s proxy
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
* `whitelist`: List of domains to bypass the mediaproxy
## :gopher
* `enabled`: Enables the gopher interface
* `ip`: IP address to bind to
* `port`: Port to bind to
* `dstport`: Port advertised in urls (optional, defaults to `port`)
## Pleroma.Web.Endpoint
!!! note
`Phoenix` endpoint configuration, all configuration options can be viewed [here](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#module-dynamic-configuration), only common options are listed here.
* `http` - a list containing http protocol configuration, all configuration options can be viewed [here](https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html#module-options), only common options are listed here. For deployment using docker, you need to set this to `[ip: {0,0,0,0}, port: 4000]` to make pleroma accessible from other containers (such as your nginx server).
- `ip` - a tuple consisting of 4 integers
- `port`
* `url` - a list containing the configuration for generating urls, accepts
- `host` - the host without the scheme and a post (e.g `example.com`, not `https://example.com:2020`)
- `scheme` - e.g `http`, `https`
- `port`
- `path`
* `extra_cookie_attrs` - a list of `Key=Value` strings to be added as non-standard cookie attributes. Defaults to `["SameSite=Lax"]`. See the [SameSite article](https://www.owasp.org/index.php/SameSite) on OWASP for more info.
!!! warning
If you modify anything inside these lists, default `config.exs` values will be overwritten, which may result in breakage, to make sure this does not happen please copy the default value for the list from `config.exs` and modify/add only what you need
Example:
```elixir
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "example.com", port: 2020, scheme: "https"],
http: [
# start copied from config.exs
dispatch: [
{:_,
[
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
{Phoenix.Transports.WebSocket,
{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
]}
# end copied from config.exs
],
port: 8080,
ip: {127, 0, 0, 1}
]
```
This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls starting with `https://example.com:2020`
## :activitypub
* ``unfollow_blocked``: Whether blocks result in people getting unfollowed
* ``outgoing_blocks``: Whether to federate blocks to other instances
* ``deny_follow_blocked``: Whether to disallow following an account that has blocked the user in question
* ``sign_object_fetches``: Sign object fetches with HTTP signatures
## :http_security
* ``enabled``: Whether the managed content security policy is enabled
* ``sts``: Whether to additionally send a `Strict-Transport-Security` header
* ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent
* ``ct_max_age``: The maximum age for the `Expect-CT` header if sent
* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`
* ``report_uri``: Adds the specified url to `report-uri` and `report-to` group in CSP header.
## :mrf_user_allowlist
The keys in this section are the domain names that the policy should apply to.
Each key should be assigned a list of users that should be allowed through by
their ActivityPub ID.
An example:
```elixir
config :pleroma, :mrf_user_allowlist,
"example.org": ["https://example.org/users/admin"]
```
## :web_push_encryption, :vapid_details
Web Push Notifications configuration. You can use the mix task `mix web_push.gen.keypair` to generate it.
* ``subject``: a mailto link for the administrative contact. It’s best if this email is not a personal email address, but rather a group email so that if a person leaves an organization, is unavailable for an extended period, or otherwise can’t respond, someone else on the list can.
* ``public_key``: VAPID public key
* ``private_key``: VAPID private key
## Pleroma.Captcha
* `enabled`: Whether the captcha should be shown on registration
* `method`: The method/service to use for captcha
* `seconds_valid`: The time in seconds for which the captcha is valid
### Pleroma.Captcha.Kocaptcha
Kocaptcha is a very simple captcha service with a single API endpoint,
the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint
`https://captcha.kotobank.ch` is hosted by the developer.
* `endpoint`: the kocaptcha endpoint to use
## :admin_token
Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the 'admin_token' parameter. Example:
```elixir
config :pleroma, :admin_token, "somerandomtoken"
```
You can then do
```sh
curl "http://localhost:4000/api/pleroma/admin/invite_token?admin_token=somerandomtoken"
```
## Oban
[Oban](https://github.com/sorentwo/oban) asynchronous job processor configuration.
Configuration options described in [Oban readme](https://github.com/sorentwo/oban#usage):
* `repo` - app's Ecto repo (`Pleroma.Repo`)
* `verbose` - logs verbosity
* `prune` - non-retryable jobs [pruning settings](https://github.com/sorentwo/oban#pruning) (`:disabled` / `{:maxlen, value}` / `{:maxage, value}`)
* `queues` - job queues (see below)
Pleroma has the following queues:
* `activity_expiration` - Activity expiration
* `federator_outgoing` - Outgoing federation
* `federator_incoming` - Incoming federation
* `mailer` - Email sender, see [`Pleroma.Emails.Mailer`](#pleromaemailsmailer)
* `transmogrifier` - Transmogrifier
* `web_push` - Web push notifications
* `scheduled_activities` - Scheduled activities, see [`Pleroma.ScheduledActivity`](#pleromascheduledactivity)
Example:
```elixir
config :pleroma, Oban,
repo: Pleroma.Repo,
verbose: false,
prune: {:maxlen, 1500},
queues: [
federator_incoming: 50,
federator_outgoing: 50
]
```
This config contains two queues: `federator_incoming` and `federator_outgoing`. Both have the number of max concurrent jobs set to `50`.
### Migrating `pleroma_job_queue` settings
`config :pleroma_job_queue, :queues` is replaced by `config :pleroma, Oban, :queues` and uses the same format (keys are queues' names, values are max concurrent jobs numbers).
## :workers
Includes custom worker options not interpretable directly by `Oban`.
* `retries` — keyword lists where keys are `Oban` queues (see above) and values are numbers of max attempts for failed jobs.
Example:
```elixir
config :pleroma, :workers,
retries: [
federator_incoming: 5,
federator_outgoing: 5
]
```
### Migrating `Pleroma.Web.Federator.RetryQueue` settings
* `max_retries` is replaced with `config :pleroma, :workers, retries: [federator_outgoing: 5]`
* `enabled: false` corresponds to `config :pleroma, :workers, retries: [federator_outgoing: 1]`
* deprecated options: `max_jobs`, `initial_timeout`
## Pleroma.Web.Metadata
* `providers`: a list of metadata providers to enable. Providers available:
* Pleroma.Web.Metadata.Providers.OpenGraph
* Pleroma.Web.Metadata.Providers.TwitterCard
* Pleroma.Web.Metadata.Providers.RelMe - add links from user bio with rel=me into the `<header>` as `<link rel=me>`
* Pleroma.Web.Metadata.Providers.Feed - add a link to a user's Atom feed into the `<header>` as `<link rel=alternate>`
* `unfurl_nsfw`: If set to `true` nsfw attachments will be shown in previews
## :rich_media
* `enabled`: if enabled the instance will parse metadata from attached links to generate link previews
* `ignore_hosts`: list of hosts which will be ignored by the metadata parser. For example `["accounts.google.com", "xss.website"]`, defaults to `[]`.
* `ignore_tld`: list TLDs (top-level domains) which will ignore for parse metadata. default is ["local", "localdomain", "lan"]
* `parsers`: list of Rich Media parsers
## :fetch_initial_posts
* `enabled`: if enabled, when a new user is federated with, fetch some of their latest posts
* `pages`: the amount of pages to fetch
## :hackney_pools
Advanced. Tweaks Hackney (http client) connections pools.
There's three pools used:
* `:federation` for the federation jobs.
You may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs.
* `:media` for rich media, media proxy
* `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`)
For each pool, the options are:
* `max_connections` - how much connections a pool can hold
* `timeout` - retention duration for connections
## :auto_linker
Configuration for the `auto_linker` library:
* `class: "auto-linker"` - specify the class to be added to the generated link. false to clear
* `rel: "noopener noreferrer"` - override the rel attribute. false to clear
* `new_window: true` - set to false to remove `target='_blank'` attribute
* `scheme: false` - Set to true to link urls with schema `http://google.com`
* `truncate: false` - Set to a number to truncate urls longer then the number. Truncated urls will end in `..`
* `strip_prefix: true` - Strip the scheme prefix
* `extra: false` - link urls with rarely used schemes (magnet, ipfs, irc, etc.)
Example:
```elixir
config :auto_linker,
opts: [
scheme: true,
extra: true,
class: false,
strip_prefix: false,
new_window: false,
rel: "ugc"
]
```
## Pleroma.Scheduler
Configuration for [Quantum](https://github.com/quantum-elixir/quantum-core) jobs scheduler.
See [Quantum readme](https://github.com/quantum-elixir/quantum-core#usage) for the list of supported options.
Example:
```elixir
config :pleroma, Pleroma.Scheduler,
global: true,
overlap: true,
timezone: :utc,
jobs: [{"0 */6 * * * *", {Pleroma.Web.Websub, :refresh_subscriptions, []}}]
```
The above example defines a single job which invokes `Pleroma.Web.Websub.refresh_subscriptions()` every 6 hours ("0 */6 * * * *", [crontab format](https://en.wikipedia.org/wiki/Cron)).
## Pleroma.ScheduledActivity
* `daily_user_limit`: the number of scheduled activities a user is allowed to create in a single day (Default: `25`)
* `total_user_limit`: the number of scheduled activities a user is allowed to create in total (Default: `300`)
* `enabled`: whether scheduled activities are sent to the job queue to be executed
## Pleroma.ActivityExpiration
* `enabled`: whether expired activities will be sent to the job queue to be deleted
## Pleroma.Web.Auth.Authenticator
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication
## :ldap
Use LDAP for user authentication. When a user logs in to the Pleroma
instance, the name and password will be verified by trying to authenticate
(bind) to an LDAP server. If a user exists in the LDAP directory but there
is no account with the same name yet on the Pleroma instance then a new
Pleroma account will be created with the same name as the LDAP user name.
* `enabled`: enables LDAP authentication
* `host`: LDAP server hostname
* `port`: LDAP port, e.g. 389 or 636
* `ssl`: true to use SSL, usually implies the port 636
* `sslopts`: additional SSL options
* `tls`: true to start TLS, usually implies the port 389
* `tlsopts`: additional TLS options
* `base`: LDAP base, e.g. "dc=example,dc=com"
* `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base"
## BBS / SSH access
To enable simple command line interface accessible over ssh, add a setting like this to your configuration file:
```exs
app_dir = File.cwd!
priv_dir = Path.join([app_dir, "priv/ssh_keys"])
config :esshd,
enabled: true,
priv_dir: priv_dir,
handler: "Pleroma.BBS.Handler",
port: 10_022,
password_authenticator: "Pleroma.BBS.Authenticator"
```
Feel free to adjust the priv_dir and port number. Then you will have to create the key for the keys (in the example `priv/ssh_keys`) and create the host keys with `ssh-keygen -m PEM -N "" -b 2048 -t rsa -f ssh_host_rsa_key`. After restarting, you should be able to connect to your Pleroma instance with `ssh username@server -p $PORT`
## :auth
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication
Authentication / authorization settings.
* `auth_template`: authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.eex`.
* `oauth_consumer_template`: OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`.
* `oauth_consumer_strategies`: the list of enabled OAuth consumer strategies; by default it's set by `OAUTH_CONSUMER_STRATEGIES` environment variable. Each entry in this space-delimited string should be of format `<strategy>` or `<strategy>:<dependency>` (e.g. `twitter` or `keycloak:ueberauth_keycloak_strategy` in case dependency is named differently than `ueberauth_<strategy>`).
## :email_notifications
Email notifications settings.
- digest - emails of "what you've missed" for users who have been
inactive for a while.
- active: globally enable or disable digest emails
- schedule: When to send digest email, in [crontab format](https://en.wikipedia.org/wiki/Cron).
"0 0 * * 0" is the default, meaning "once a week at midnight on Sunday morning"
- interval: Minimum interval between digest emails to one user
- inactivity_threshold: Minimum user inactivity threshold
## Pleroma.Emails.UserEmail
- `:logo` - a path to a custom logo. Set it to `nil` to use the default Pleroma logo.
- `:styling` - a map with color settings for email templates.
## OAuth consumer mode
OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.).
Implementation is based on Ueberauth; see the list of [available strategies](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies).
!!! note
Each strategy is shipped as a separate dependency; in order to get the strategies, run `OAUTH_CONSUMER_STRATEGIES="..." mix deps.get`, e.g. `OAUTH_CONSUMER_STRATEGIES="twitter facebook google microsoft" mix deps.get`. The server should also be started with `OAUTH_CONSUMER_STRATEGIES="..." mix phx.server` in case you enable any strategies.
!!! note
Each strategy requires separate setup (on external provider side and Pleroma side). Below are the guidelines on setting up most popular strategies.
!!! note
Make sure that `"SameSite=Lax"` is set in `extra_cookie_attrs` when you have this feature enabled. OAuth consumer mode will not work with `"SameSite=Strict"`
* For Twitter, [register an app](https://developer.twitter.com/en/apps), configure callback URL to https://<your_host>/oauth/twitter/callback
* For Facebook, [register an app](https://developers.facebook.com/apps), configure callback URL to https://<your_host>/oauth/facebook/callback, enable Facebook Login service at https://developers.facebook.com/apps/<app_id>/fb-login/settings/
* For Google, [register an app](https://console.developers.google.com), configure callback URL to https://<your_host>/oauth/google/callback
* For Microsoft, [register an app](https://portal.azure.com), configure callback URL to https://<your_host>/oauth/microsoft/callback
Once the app is configured on external OAuth provider side, add app's credentials and strategy-specific settings (if any — e.g. see Microsoft below) to `config/prod.secret.exs`,
per strategy's documentation (e.g. [ueberauth_twitter](https://github.com/ueberauth/ueberauth_twitter)). Example config basing on environment variables:
```elixir
# Twitter
config :ueberauth, Ueberauth.Strategy.Twitter.OAuth,
consumer_key: System.get_env("TWITTER_CONSUMER_KEY"),
consumer_secret: System.get_env("TWITTER_CONSUMER_SECRET")
# Facebook
config :ueberauth, Ueberauth.Strategy.Facebook.OAuth,
client_id: System.get_env("FACEBOOK_APP_ID"),
client_secret: System.get_env("FACEBOOK_APP_SECRET"),
redirect_uri: System.get_env("FACEBOOK_REDIRECT_URI")
# Google
config :ueberauth, Ueberauth.Strategy.Google.OAuth,
client_id: System.get_env("GOOGLE_CLIENT_ID"),
client_secret: System.get_env("GOOGLE_CLIENT_SECRET"),
redirect_uri: System.get_env("GOOGLE_REDIRECT_URI")
# Microsoft
config :ueberauth, Ueberauth.Strategy.Microsoft.OAuth,
client_id: System.get_env("MICROSOFT_CLIENT_ID"),
client_secret: System.get_env("MICROSOFT_CLIENT_SECRET")
config :ueberauth, Ueberauth,
providers: [
microsoft: {Ueberauth.Strategy.Microsoft, [callback_params: []]}
]
# Keycloak
# Note: make sure to add `keycloak:ueberauth_keycloak_strategy` entry to `OAUTH_CONSUMER_STRATEGIES` environment variable
keycloak_url = "https://publicly-reachable-keycloak-instance.org:8080"
config :ueberauth, Ueberauth.Strategy.Keycloak.OAuth,
client_id: System.get_env("KEYCLOAK_CLIENT_ID"),
client_secret: System.get_env("KEYCLOAK_CLIENT_SECRET"),
site: keycloak_url,
authorize_url: "#{keycloak_url}/auth/realms/master/protocol/openid-connect/auth",
token_url: "#{keycloak_url}/auth/realms/master/protocol/openid-connect/token",
userinfo_url: "#{keycloak_url}/auth/realms/master/protocol/openid-connect/userinfo",
token_method: :post
config :ueberauth, Ueberauth,
providers: [
keycloak: {Ueberauth.Strategy.Keycloak, [uid_field: :email]}
]
```
## OAuth 2.0 provider - :oauth2
Configure OAuth 2 provider capabilities:
* `token_expires_in` - The lifetime in seconds of the access token.
* `issue_new_refresh_token` - Keeps old refresh token or generate new refresh token when to obtain an access token.
* `clean_expired_tokens` - Enable a background job to clean expired oauth tokens. Defaults to `false`.
* `clean_expired_tokens_interval` - Interval to run the job to clean expired tokens. Defaults to `86_400_000` (24 hours).
## :emoji
* `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]`
* `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]`
* `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]`
* `default_manifest`: Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays).
* `shared_pack_cache_seconds_per_file`: When an emoji pack is shared, the archive is created and cached in
memory for this amount of seconds multiplied by the number of files.
## Database options
### RUM indexing for full text search
* `rum_enabled`: If RUM indexes should be used. Defaults to `false`.
RUM indexes are an alternative indexing scheme that is not included in PostgreSQL by default. While they may eventually be mainlined, for now they have to be installed as a PostgreSQL extension from https://github.com/postgrespro/rum.
Their advantage over the standard GIN indexes is that they allow efficient ordering of search results by timestamp, which makes search queries a lot faster on larger servers, by one or two orders of magnitude. They take up around 3 times as much space as GIN indexes.
To enable them, both the `rum_enabled` flag has to be set and the following special migration has to be run:
`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`
This will probably take a long time.
## :rate_limit
This is an advanced feature and disabled by default.
If your instance is behind a reverse proxy you must enable and configure [`Pleroma.Plugs.RemoteIp`](#pleroma-plugs-remoteip).
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
* The first element: `scale` (Integer). The time scale in milliseconds.
* The second element: `limit` (Integer). How many requests to limit in the time scale provided.
It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
Supported rate limiters:
* `:search` for the search requests (account & status search etc.)
* `:app_account_creation` for registering user accounts from the same IP address
* `:relations_actions` for actions on relations with all users (follow, unfollow)
* `:relation_id_action` for actions on relation with a specific user (follow, unfollow)
* `:statuses_actions` for create / delete / fav / unfav / reblog / unreblog actions on any statuses
* `:status_id_action` for fav / unfav or reblog / unreblog actions on the same status by the same user
## :web_cache_ttl
The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration.
Available caches:
* `:activity_pub` - activity pub routes (except question activities). Defaults to `nil` (no expiration).
* `:activity_pub_question` - activity pub routes (question activities). Defaults to `30_000` (30 seconds).
## Pleroma.Plugs.RemoteIp
!!! warning
If your instance is not behind at least one reverse proxy, you should not enable this plug.
`Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
Available options:
* `enabled` - Enable/disable the plug. Defaults to `false`.
* `headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`.
* `proxies` - A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`.
* `reserved` - Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network).
diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex
index cfd9eeada..8a827ca80 100644
--- a/lib/mix/tasks/pleroma/database.ex
+++ b/lib/mix/tasks/pleroma/database.ex
@@ -1,138 +1,138 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Database do
alias Pleroma.Conversation
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
require Logger
require Pleroma.Constants
import Mix.Pleroma
use Mix.Task
@shortdoc "A collection of database related tasks"
@moduledoc File.read!("docs/administration/CLI_tasks/database.md")
def run(["remove_embedded_objects" | args]) do
{options, [], []} =
OptionParser.parse(
args,
strict: [
vacuum: :boolean
]
)
start_pleroma()
Logger.info("Removing embedded objects")
Repo.query!(
- "update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
+ "update activities set data = safe_jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
[],
timeout: :infinity
)
if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL")
Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
end
end
def run(["bump_all_conversations"]) do
start_pleroma()
Conversation.bump_for_all_activities()
end
def run(["update_users_following_followers_counts"]) do
start_pleroma()
users = Repo.all(User)
Enum.each(users, &User.remove_duplicated_following/1)
Enum.each(users, &User.update_follower_count/1)
end
def run(["prune_objects" | args]) do
import Ecto.Query
{options, [], []} =
OptionParser.parse(
args,
strict: [
vacuum: :boolean
]
)
start_pleroma()
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days])
Logger.info("Pruning objects older than #{deadline} days")
time_deadline =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(-(deadline * 86_400))
from(o in Object,
where:
fragment(
"?->'to' \\? ? OR ?->'cc' \\? ?",
o.data,
^Pleroma.Constants.as_public(),
o.data,
^Pleroma.Constants.as_public()
),
where: o.inserted_at < ^time_deadline,
where:
fragment("split_part(?->>'actor', '/', 3) != ?", o.data, ^Pleroma.Web.Endpoint.host())
)
|> Repo.delete_all(timeout: :infinity)
if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL")
Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
end
end
def run(["fix_likes_collections"]) do
import Ecto.Query
start_pleroma()
from(object in Object,
where: fragment("(?)->>'likes' is not null", object.data),
select: %{id: object.id, likes: fragment("(?)->>'likes'", object.data)}
)
|> Pleroma.RepoStreamer.chunk_stream(100)
|> Stream.each(fn objects ->
ids =
objects
|> Enum.filter(fn object -> object.likes |> Jason.decode!() |> is_map() end)
|> Enum.map(& &1.id)
Object
|> where([object], object.id in ^ids)
|> update([object],
set: [
data:
fragment(
- "jsonb_set(?, '{likes}', '[]'::jsonb, true)",
+ "safe_jsonb_set(?, '{likes}', '[]'::jsonb, true)",
object.data
)
]
)
|> Repo.update_all([], timeout: :infinity)
end)
|> Stream.run()
end
end
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index 6ef0a635d..35669af27 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -1,254 +1,256 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Emoji do
use Mix.Task
@shortdoc "Manages emoji packs"
@moduledoc File.read!("docs/administration/CLI_tasks/emoji.md")
def run(["ls-packs" | args]) do
Application.ensure_all_started(:hackney)
{options, [], []} = parse_global_opts(args)
manifest =
fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest())
Enum.each(manifest, fn {name, info} ->
to_print = [
{"Name", name},
{"Homepage", info["homepage"]},
{"Description", info["description"]},
{"License", info["license"]},
{"Source", info["src"]}
]
for {param, value} <- to_print do
IO.puts(IO.ANSI.format([:bright, param, :normal, ": ", value]))
end
# A newline
IO.puts("")
end)
end
def run(["get-packs" | args]) do
Application.ensure_all_started(:hackney)
{options, pack_names, []} = parse_global_opts(args)
manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest()
manifest = fetch_manifest(manifest_url)
for pack_name <- pack_names do
if Map.has_key?(manifest, pack_name) do
pack = manifest[pack_name]
src_url = pack["src"]
IO.puts(
IO.ANSI.format([
"Downloading ",
:bright,
pack_name,
:normal,
" from ",
:underline,
src_url
])
)
binary_archive = Tesla.get!(client(), src_url).body
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
if archive_sha == String.upcase(pack["src_sha256"]) do
IO.puts(IO.ANSI.format(sha_status_text ++ [:green, "OK"]))
else
IO.puts(IO.ANSI.format(sha_status_text ++ [:red, "BAD"]))
raise "Bad SHA256 for #{pack_name}"
end
# The url specified in files should be in the same directory
files_url = Path.join(Path.dirname(manifest_url), pack["files"])
IO.puts(
IO.ANSI.format([
"Fetching the file list for ",
:bright,
pack_name,
:normal,
" from ",
:underline,
files_url
])
)
files = Tesla.get!(client(), files_url).body |> Jason.decode!()
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
pack_path =
Path.join([
Pleroma.Config.get!([:instance, :static_dir]),
"emoji",
pack_name
])
files_to_unzip =
Enum.map(
files,
fn {_, f} -> to_charlist(f) end
)
{:ok, _} =
:zip.unzip(binary_archive,
cwd: pack_path,
file_list: files_to_unzip
)
- IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name]))
-
- emoji_txt_str =
- Enum.map(
- files,
- fn {shortcode, path} ->
- emojo_path = Path.join("/emoji/#{pack_name}", path)
- "#{shortcode}, #{emojo_path}"
- end
- )
- |> Enum.join("\n")
-
- File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str)
+ IO.puts(IO.ANSI.format(["Writing pack.json for ", :bright, pack_name]))
+
+ pack_json = %{
+ pack: %{
+ "license" => pack["license"],
+ "homepage" => pack["homepage"],
+ "description" => pack["description"],
+ "fallback-src" => pack["src"],
+ "fallback-src-sha256" => pack["src_sha256"],
+ "share-files" => true
+ },
+ files: files
+ }
+
+ File.write!(Path.join(pack_path, "pack.json"), Jason.encode!(pack_json, pretty: true))
else
IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
end
end
end
def run(["gen-pack", src]) do
Application.ensure_all_started(:hackney)
proposed_name = Path.basename(src) |> Path.rootname()
name = String.trim(IO.gets("Pack name [#{proposed_name}]: "))
# If there's no name, use the default one
name = if String.length(name) > 0, do: name, else: proposed_name
license = String.trim(IO.gets("License: "))
homepage = String.trim(IO.gets("Homepage: "))
description = String.trim(IO.gets("Description: "))
proposed_files_name = "#{name}.json"
files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: "))
files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name
default_exts = [".png", ".gif"]
default_exts_str = Enum.join(default_exts, " ")
exts =
String.trim(
IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ")
)
exts =
if String.length(exts) > 0 do
String.split(exts, " ")
|> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
else
default_exts
end
IO.puts("Downloading the pack and generating SHA256")
binary_archive = Tesla.get!(client(), src).body
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
IO.puts("SHA256 is #{archive_sha}")
pack_json = %{
name => %{
license: license,
homepage: homepage,
description: description,
src: src,
src_sha256: archive_sha,
files: files_name
}
}
tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")
{:ok, _} =
:zip.unzip(
binary_archive,
cwd: tmp_pack_dir
)
emoji_map = Pleroma.Emoji.Loader.make_shortcode_to_file_map(tmp_pack_dir, exts)
File.write!(files_name, Jason.encode!(emoji_map, pretty: true))
IO.puts("""
#{files_name} has been created and contains the list of all found emojis in the pack.
Please review the files in the remove those not needed.
""")
if File.exists?("index.json") do
existing_data = File.read!("index.json") |> Jason.decode!()
File.write!(
"index.json",
Jason.encode!(
Map.merge(
existing_data,
pack_json
),
pretty: true
)
)
IO.puts("index.json file has been update with the #{name} pack")
else
File.write!("index.json", Jason.encode!(pack_json, pretty: true))
IO.puts("index.json has been created with the #{name} pack")
end
end
defp fetch_manifest(from) do
Jason.decode!(
if String.starts_with?(from, "http") do
Tesla.get!(client(), from).body
else
File.read!(from)
end
)
end
defp parse_global_opts(args) do
OptionParser.parse(
args,
strict: [
manifest: :string
],
aliases: [
m: :manifest
]
)
end
defp client do
middleware = [
{Tesla.Middleware.FollowRedirects, [max_redirects: 3]}
]
Tesla.client(middleware)
end
defp default_manifest, do: Pleroma.Config.get!([:emoji, :default_manifest])
end
diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex
index d7a7b599f..7ef5f9678 100644
--- a/lib/mix/tasks/pleroma/relay.ex
+++ b/lib/mix/tasks/pleroma/relay.ex
@@ -1,48 +1,44 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Relay do
use Mix.Task
import Mix.Pleroma
- alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
@shortdoc "Manages remote relays"
@moduledoc File.read!("docs/administration/CLI_tasks/relay.md")
def run(["follow", target]) do
start_pleroma()
with {:ok, _activity} <- Relay.follow(target) do
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
else
{:error, e} -> shell_error("Error while following #{target}: #{inspect(e)}")
end
end
def run(["unfollow", target]) do
start_pleroma()
with {:ok, _activity} <- Relay.unfollow(target) do
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
else
{:error, e} -> shell_error("Error while following #{target}: #{inspect(e)}")
end
end
def run(["list"]) do
start_pleroma()
- with %User{following: following} = _user <- Relay.get_actor() do
- following
- |> Enum.map(fn entry -> URI.parse(entry).host end)
- |> Enum.uniq()
- |> Enum.each(&shell_info(&1))
+ with {:ok, list} <- Relay.list() do
+ list |> Enum.each(&shell_info(&1))
else
- e -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
+ {:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
end
end
end
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index 134b5bccc..d7bdc2310 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -1,386 +1,396 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.User do
use Mix.Task
import Mix.Pleroma
+ alias Ecto.Changeset
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.OAuth
@shortdoc "Manages Pleroma users"
@moduledoc File.read!("docs/administration/CLI_tasks/user.md")
def run(["new", nickname, email | rest]) do
{options, [], []} =
OptionParser.parse(
rest,
strict: [
name: :string,
bio: :string,
password: :string,
moderator: :boolean,
admin: :boolean,
assume_yes: :boolean
],
aliases: [
y: :assume_yes
]
)
name = Keyword.get(options, :name, nickname)
bio = Keyword.get(options, :bio, "")
{password, generated_password?} =
case Keyword.get(options, :password) do
nil ->
{:crypto.strong_rand_bytes(16) |> Base.encode64(), true}
password ->
{password, false}
end
moderator? = Keyword.get(options, :moderator, false)
admin? = Keyword.get(options, :admin, false)
assume_yes? = Keyword.get(options, :assume_yes, false)
shell_info("""
A user will be created with the following information:
- nickname: #{nickname}
- email: #{email}
- password: #{
if(generated_password?, do: "[generated; a reset link will be created]", else: password)
}
- name: #{name}
- bio: #{bio}
- moderator: #{if(moderator?, do: "true", else: "false")}
- admin: #{if(admin?, do: "true", else: "false")}
""")
proceed? = assume_yes? or shell_yes?("Continue?")
if proceed? do
start_pleroma()
params = %{
nickname: nickname,
email: email,
password: password,
password_confirmation: password,
name: name,
bio: bio
}
changeset = User.register_changeset(%User{}, params, need_confirmation: false)
{:ok, _user} = User.register(changeset)
shell_info("User #{nickname} created")
if moderator? do
run(["set", nickname, "--moderator"])
end
if admin? do
run(["set", nickname, "--admin"])
end
if generated_password? do
run(["reset_password", nickname])
end
else
shell_info("User will not be created.")
end
end
def run(["rm", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.perform(:delete, user)
shell_info("User #{nickname} deleted.")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["toggle_activated", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
- {:ok, user} = User.deactivate(user, !user.info.deactivated)
+ {:ok, user} = User.deactivate(user, !user.deactivated)
shell_info(
- "Activation status of #{nickname}: #{if(user.info.deactivated, do: "de", else: "")}activated"
+ "Activation status of #{nickname}: #{if(user.deactivated, do: "de", else: "")}activated"
)
else
_ ->
shell_error("No user #{nickname}")
end
end
def run(["reset_password", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
shell_info("Generated password reset token for #{user.nickname}")
IO.puts(
"URL: #{
Pleroma.Web.Router.Helpers.reset_password_url(
Pleroma.Web.Endpoint,
:reset,
token.token
)
}"
)
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["unsubscribe", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
shell_info("Deactivating #{user.nickname}")
User.deactivate(user)
user
|> User.get_friends()
|> Enum.each(fn friend ->
user = User.get_cached_by_id(user.id)
shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
User.unfollow(user, friend)
end)
:timer.sleep(500)
user = User.get_cached_by_id(user.id)
if Enum.empty?(user.following) do
shell_info("Successfully unsubscribed all followers from #{user.nickname}")
end
else
_ ->
shell_error("No user #{nickname}")
end
end
def run(["unsubscribe_all_from_instance", instance]) do
start_pleroma()
Pleroma.User.Query.build(%{nickname: "@#{instance}"})
|> Pleroma.RepoStreamer.chunk_stream(500)
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
run(["unsubscribe", user.nickname])
end)
end)
|> Stream.run()
end
def run(["set", nickname | rest]) do
start_pleroma()
{options, [], []} =
OptionParser.parse(
rest,
strict: [
moderator: :boolean,
admin: :boolean,
locked: :boolean
]
)
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user =
case Keyword.get(options, :moderator) do
nil -> user
value -> set_moderator(user, value)
end
user =
case Keyword.get(options, :locked) do
nil -> user
value -> set_locked(user, value)
end
_user =
case Keyword.get(options, :admin) do
nil -> user
value -> set_admin(user, value)
end
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["tag", nickname | tags]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
else
_ ->
shell_error("Could not change user tags for #{nickname}")
end
end
def run(["untag", nickname | tags]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
else
_ ->
shell_error("Could not change user tags for #{nickname}")
end
end
def run(["invite" | rest]) do
{options, [], []} =
OptionParser.parse(rest,
strict: [
expires_at: :string,
max_use: :integer
]
)
options =
options
|> Keyword.update(:expires_at, {:ok, nil}, fn
nil -> {:ok, nil}
val -> Date.from_iso8601(val)
end)
|> Enum.into(%{})
start_pleroma()
with {:ok, val} <- options[:expires_at],
options = Map.put(options, :expires_at, val),
{:ok, invite} <- UserInviteToken.create_invite(options) do
shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " "))
url =
Pleroma.Web.Router.Helpers.redirect_url(
Pleroma.Web.Endpoint,
:registration_page,
invite.token
)
IO.puts(url)
else
error ->
shell_error("Could not create invite token: #{inspect(error)}")
end
end
def run(["invites"]) do
start_pleroma()
shell_info("Invites list:")
UserInviteToken.list_invites()
|> Enum.each(fn invite ->
expire_info =
with expires_at when not is_nil(expires_at) <- invite.expires_at do
" | Expires at: #{Date.to_string(expires_at)}"
end
using_info =
with max_use when not is_nil(max_use) <- invite.max_use do
" | Max use: #{max_use} Left use: #{max_use - invite.uses}"
end
shell_info(
"ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{
invite.used
}#{expire_info}#{using_info}"
)
end)
end
def run(["revoke_invite", token]) do
start_pleroma()
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do
shell_info("Invite for token #{token} was revoked.")
else
_ -> shell_error("No invite found with token #{token}")
end
end
def run(["delete_activities", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete_user_activities(user)
shell_info("User #{nickname} statuses deleted.")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["toggle_confirmed", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.toggle_confirmation(user)
- message = if user.info.confirmation_pending, do: "needs", else: "doesn't need"
+ message = if user.confirmation_pending, do: "needs", else: "doesn't need"
shell_info("#{nickname} #{message} confirmation.")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["sign_out", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
OAuth.Token.delete_user_tokens(user)
OAuth.Authorization.delete_user_authorizations(user)
shell_info("#{nickname} signed out from all apps.")
else
_ ->
shell_error("No local user #{nickname}")
end
end
defp set_moderator(user, value) do
- {:ok, user} = User.update_info(user, &User.Info.admin_api_update(&1, %{is_moderator: value}))
+ {:ok, user} =
+ user
+ |> Changeset.change(%{is_moderator: value})
+ |> User.update_and_set_cache()
- shell_info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")
+ shell_info("Moderator status of #{user.nickname}: #{user.is_moderator}")
user
end
defp set_admin(user, value) do
- {:ok, user} = User.update_info(user, &User.Info.admin_api_update(&1, %{is_admin: value}))
+ {:ok, user} =
+ user
+ |> Changeset.change(%{is_admin: value})
+ |> User.update_and_set_cache()
- shell_info("Admin status of #{user.nickname}: #{user.info.is_admin}")
+ shell_info("Admin status of #{user.nickname}: #{user.is_admin}")
user
end
defp set_locked(user, value) do
- {:ok, user} = User.update_info(user, &User.Info.user_upgrade(&1, %{locked: value}))
+ {:ok, user} =
+ user
+ |> Changeset.change(%{locked: value})
+ |> User.update_and_set_cache()
- shell_info("Locked status of #{user.nickname}: #{user.info.locked}")
+ shell_info("Locked status of #{user.nickname}: #{user.locked}")
user
end
end
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 0bf218bc7..d681eecc8 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -1,192 +1,182 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Application do
import Cachex.Spec
use Application
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@repository Mix.Project.config()[:source_url]
@env Mix.env()
def name, do: @name
def version, do: @version
def named_version, do: @name <> " " <> @version
def repository, do: @repository
def user_agent do
info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
named_version() <> "; " <> info
end
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
Pleroma.Config.DeprecationWarnings.warn()
setup_instrumenters()
# Define workers and child supervisors to be supervised
children =
[
Pleroma.Repo,
Pleroma.Scheduler,
Pleroma.Config.TransferTask,
Pleroma.Emoji,
Pleroma.Captcha,
Pleroma.Daemons.ScheduledActivityDaemon,
Pleroma.Daemons.ActivityExpirationDaemon
] ++
cachex_children() ++
hackney_pool_children() ++
[
Pleroma.Stats,
Pleroma.JobQueueMonitor,
{Oban, Pleroma.Config.get(Oban)}
] ++
task_children(@env) ++
oauth_cleanup_child(oauth_cleanup_enabled?()) ++
streamer_child(@env) ++
chat_child(@env, chat_enabled?()) ++
[
Pleroma.Web.Endpoint,
Pleroma.Gopher.Server
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
Supervisor.start_link(children, opts)
end
defp setup_instrumenters do
require Prometheus.Registry
if Application.get_env(:prometheus, Pleroma.Repo.Instrumenter) do
:ok =
:telemetry.attach(
"prometheus-ecto",
[:pleroma, :repo, :query],
&Pleroma.Repo.Instrumenter.handle_event/4,
%{}
)
Pleroma.Repo.Instrumenter.setup()
end
Pleroma.Web.Endpoint.MetricsExporter.setup()
Pleroma.Web.Endpoint.PipelineInstrumenter.setup()
Pleroma.Web.Endpoint.Instrumenter.setup()
end
def enabled_hackney_pools do
[:media] ++
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
[:federation]
else
[]
end ++
if Pleroma.Config.get([Pleroma.Upload, :proxy_remote]) do
[:upload]
else
[]
end
end
defp cachex_children do
[
build_cachex("used_captcha", ttl_interval: seconds_valid_interval()),
build_cachex("user", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
build_cachex("object", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),
build_cachex("scrubber", limit: 2500),
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
build_cachex("failed_proxy_url", limit: 2500)
]
end
defp emoji_packs_expiration,
do: expiration(default: :timer.seconds(5 * 60), interval: :timer.seconds(60))
defp idempotency_expiration,
do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
defp seconds_valid_interval,
do: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
defp build_cachex(type, opts),
do: %{
id: String.to_atom("cachex_" <> type),
start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
type: :worker
}
defp chat_enabled?, do: Pleroma.Config.get([:chat, :enabled])
defp oauth_cleanup_enabled?,
do: Pleroma.Config.get([:oauth2, :clean_expired_tokens], false)
defp streamer_child(:test), do: []
defp streamer_child(_) do
[Pleroma.Web.Streamer.supervisor()]
end
defp oauth_cleanup_child(true),
do: [Pleroma.Web.OAuth.Token.CleanWorker]
defp oauth_cleanup_child(_), do: []
defp chat_child(:test, _), do: []
defp chat_child(_env, true) do
[Pleroma.Web.ChatChannel.ChatChannelState]
end
defp chat_child(_, _), do: []
defp hackney_pool_children do
for pool <- enabled_hackney_pools() do
options = Pleroma.Config.get([:hackney_pools, pool])
:hackney_pool.child_spec(pool, options)
end
end
defp task_children(:test) do
[
%{
id: :web_push_init,
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
- },
- %{
- id: :federator_init,
- start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
- restart: :temporary
}
]
end
defp task_children(_) do
[
%{
id: :web_push_init,
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
},
- %{
- id: :federator_init,
- start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
- restart: :temporary
- },
%{
id: :internal_fetch_init,
start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
restart: :temporary
}
]
end
end
diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex
index 098016af2..ade3a526a 100644
--- a/lib/pleroma/conversation.ex
+++ b/lib/pleroma/conversation.ex
@@ -1,105 +1,111 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation do
alias Pleroma.Conversation.Participation
alias Pleroma.Conversation.Participation.RecipientShip
alias Pleroma.Repo
alias Pleroma.User
use Ecto.Schema
import Ecto.Changeset
schema "conversations" do
# This is the context ap id.
field(:ap_id, :string)
has_many(:participations, Participation)
has_many(:users, through: [:participations, :user])
timestamps()
end
def creation_cng(struct, params) do
struct
|> cast(params, [:ap_id])
|> validate_required([:ap_id])
|> unique_constraint(:ap_id)
end
def create_for_ap_id(ap_id) do
%__MODULE__{}
|> creation_cng(%{ap_id: ap_id})
|> Repo.insert(
on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]],
returning: true,
conflict_target: :ap_id
)
end
def get_for_ap_id(ap_id) do
Repo.get_by(__MODULE__, ap_id: ap_id)
end
def maybe_create_recipientships(participation, activity) do
participation = Repo.preload(participation, :recipients)
if participation.recipients |> Enum.empty?() do
recipients = User.get_all_by_ap_id(activity.recipients)
RecipientShip.create(recipients, participation)
end
end
@doc """
This will
1. Create a conversation if there isn't one already
2. Create a participation for all the people involved who don't have one already
3. Bump all relevant participations to 'unread'
"""
def create_or_bump_for(activity, opts \\ []) do
with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
"Create" <- activity.data["type"],
object <- Pleroma.Object.normalize(activity),
true <- object.data["type"] in ["Note", "Question"],
ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
{:ok, conversation} = create_for_ap_id(ap_id)
users = User.get_users_from_set(activity.recipients, false)
participations =
Enum.map(users, fn user ->
- User.increment_unread_conversation_count(conversation, user)
+ invisible_conversation = Enum.any?(users, &User.blocks?(user, &1))
+
+ unless invisible_conversation do
+ User.increment_unread_conversation_count(conversation, user)
+ end
+
+ opts = Keyword.put(opts, :invisible_conversation, invisible_conversation)
{:ok, participation} =
Participation.create_for_user_and_conversation(user, conversation, opts)
maybe_create_recipientships(participation, activity)
participation
end)
{:ok,
%{
conversation
| participations: participations
}}
else
e -> {:error, e}
end
end
@doc """
This is only meant to be run by a mix task. It creates conversations/participations for all direct messages in the database.
"""
def bump_for_all_activities do
stream =
Pleroma.Web.ActivityPub.ActivityPub.fetch_direct_messages_query()
|> Repo.stream()
Repo.transaction(
fn ->
stream
|> Enum.each(fn a -> create_or_bump_for(a, read: true) end)
end,
timeout: :infinity
)
end
end
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index ab81f3217..176b82a20 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -1,155 +1,202 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation.Participation do
use Ecto.Schema
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation.RecipientShip
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
import Ecto.Changeset
import Ecto.Query
schema "conversation_participations" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:conversation, Conversation)
field(:read, :boolean, default: false)
field(:last_activity_id, FlakeId.Ecto.CompatType, virtual: true)
has_many(:recipient_ships, RecipientShip)
has_many(:recipients, through: [:recipient_ships, :user])
timestamps()
end
def creation_cng(struct, params) do
struct
|> cast(params, [:user_id, :conversation_id, :read])
|> validate_required([:user_id, :conversation_id])
end
def create_for_user_and_conversation(user, conversation, opts \\ []) do
read = !!opts[:read]
+ invisible_conversation = !!opts[:invisible_conversation]
+
+ update_on_conflict =
+ if(invisible_conversation, do: [], else: [read: read])
+ |> Keyword.put(:updated_at, NaiveDateTime.utc_now())
%__MODULE__{}
- |> creation_cng(%{user_id: user.id, conversation_id: conversation.id, read: read})
+ |> creation_cng(%{
+ user_id: user.id,
+ conversation_id: conversation.id,
+ read: invisible_conversation || read
+ })
|> Repo.insert(
- on_conflict: [set: [read: read, updated_at: NaiveDateTime.utc_now()]],
+ on_conflict: [set: update_on_conflict],
returning: true,
conflict_target: [:user_id, :conversation_id]
)
end
def read_cng(struct, params) do
struct
|> cast(params, [:read])
|> validate_required([:read])
end
+ def mark_as_read(%User{} = user, %Conversation{} = conversation) do
+ with %__MODULE__{} = participation <- for_user_and_conversation(user, conversation) do
+ mark_as_read(participation)
+ end
+ end
+
def mark_as_read(participation) do
participation
|> read_cng(%{read: true})
|> Repo.update()
|> case do
{:ok, participation} ->
participation = Repo.preload(participation, :user)
User.set_unread_conversation_count(participation.user)
{:ok, participation}
error ->
error
end
end
+ def mark_all_as_read(%User{local: true} = user, %User{} = target_user) do
+ target_conversation_ids =
+ __MODULE__
+ |> where([p], p.user_id == ^target_user.id)
+ |> select([p], p.conversation_id)
+ |> Repo.all()
+
+ __MODULE__
+ |> where([p], p.user_id == ^user.id)
+ |> where([p], p.conversation_id in ^target_conversation_ids)
+ |> update([p], set: [read: true])
+ |> Repo.update_all([])
+
+ {:ok, user} = User.set_unread_conversation_count(user)
+ {:ok, user, []}
+ end
+
+ def mark_all_as_read(%User{} = user, %User{}), do: {:ok, user, []}
+
+ def mark_all_as_read(%User{} = user) do
+ {_, participations} =
+ __MODULE__
+ |> where([p], p.user_id == ^user.id)
+ |> where([p], not p.read)
+ |> update([p], set: [read: true])
+ |> select([p], p)
+ |> Repo.update_all([])
+
+ {:ok, user} = User.set_unread_conversation_count(user)
+ {:ok, user, participations}
+ end
+
def mark_as_unread(participation) do
participation
|> read_cng(%{read: false})
|> Repo.update()
end
def for_user(user, params \\ %{}) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
order_by: [desc: p.updated_at],
preload: [conversation: [:users]]
)
|> Pleroma.Pagination.fetch_paginated(params)
end
def for_user_and_conversation(user, conversation) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
where: p.conversation_id == ^conversation.id
)
|> Repo.one()
end
def for_user_with_last_activity_id(user, params \\ %{}) do
for_user(user, params)
|> Enum.map(fn participation ->
activity_id =
ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
"user" => user,
"blocking_user" => user
})
%{
participation
| last_activity_id: activity_id
}
end)
|> Enum.filter(& &1.last_activity_id)
end
def get(_, _ \\ [])
def get(nil, _), do: nil
def get(id, params) do
query =
if preload = params[:preload] do
from(p in __MODULE__,
preload: ^preload
)
else
__MODULE__
end
Repo.get(query, id)
end
def set_recipients(participation, user_ids) do
user_ids =
[participation.user_id | user_ids]
|> Enum.uniq()
Repo.transaction(fn ->
query =
from(r in RecipientShip,
where: r.participation_id == ^participation.id
)
Repo.delete_all(query)
users =
from(u in User,
where: u.id in ^user_ids
)
|> Repo.all()
RecipientShip.create(users, participation)
:ok
end)
{:ok, Repo.preload(participation, :recipients, force: true)}
end
def unread_conversation_count_for_user(user) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
where: not p.read,
select: %{count: count(p.id)}
)
end
end
diff --git a/lib/pleroma/daemons/digest_email_daemon.ex b/lib/pleroma/daemons/digest_email_daemon.ex
index 462ad2c55..b4c8eaad9 100644
--- a/lib/pleroma/daemons/digest_email_daemon.ex
+++ b/lib/pleroma/daemons/digest_email_daemon.ex
@@ -1,42 +1,42 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Daemons.DigestEmailDaemon do
alias Pleroma.Repo
alias Pleroma.Workers.DigestEmailsWorker
import Ecto.Query
def perform do
config = Pleroma.Config.get([:email_notifications, :digest])
negative_interval = -Map.fetch!(config, :interval)
inactivity_threshold = Map.fetch!(config, :inactivity_threshold)
inactive_users_query = Pleroma.User.list_inactive_users_query(inactivity_threshold)
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
from(u in inactive_users_query,
- where: fragment(~s(? #> '{"email_notifications","digest"}' @> 'true'), u.info),
+ where: fragment(~s(? ->'digest' @> 'true'), u.email_notifications),
where: u.last_digest_emailed_at < datetime_add(^now, ^negative_interval, "day"),
select: u
)
|> Repo.all()
|> Enum.each(fn user ->
DigestEmailsWorker.enqueue("digest_email", %{"user_id" => user.id})
end)
end
@doc """
Send digest email to the given user.
Updates `last_digest_emailed_at` field for the user and returns the updated user.
"""
@spec perform(Pleroma.User.t()) :: Pleroma.User.t()
def perform(user) do
with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(user) do
Pleroma.Emails.Mailer.deliver_async(email)
end
Pleroma.User.touch_last_digest_emailed_at(user)
end
end
diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex
index 40b67ff56..a10f88f93 100644
--- a/lib/pleroma/emails/user_email.ex
+++ b/lib/pleroma/emails/user_email.ex
@@ -1,178 +1,178 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.UserEmail do
@moduledoc "User emails"
use Phoenix.Swoosh, view: Pleroma.Web.EmailView, layout: {Pleroma.Web.LayoutView, :email}
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router
defp instance_name, do: Config.get([:instance, :name])
defp sender do
email = Config.get([:instance, :notify_email]) || Config.get([:instance, :email])
{instance_name(), email}
end
defp recipient(email, nil), do: email
defp recipient(email, name), do: {name, email}
defp recipient(%User{} = user), do: recipient(user.email, user.name)
def password_reset_email(user, token) when is_binary(token) do
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
html_body = """
<h3>Reset your password at #{instance_name()}</h3>
<p>Someone has requested password change for your account at #{instance_name()}.</p>
<p>If it was you, visit the following link to proceed: <a href="#{password_reset_url}">reset password</a>.</p>
<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>
"""
new()
|> to(recipient(user))
|> from(sender())
|> subject("Password reset")
|> html_body(html_body)
end
def user_invitation_email(
user,
%Pleroma.UserInviteToken{} = user_invite_token,
to_email,
to_name \\ nil
) do
registration_url =
Router.Helpers.redirect_url(
Endpoint,
:registration_page,
user_invite_token.token
)
html_body = """
<h3>You are invited to #{instance_name()}</h3>
<p>#{user.name} invites you to join #{instance_name()}, an instance of Pleroma federated social networking platform.</p>
<p>Click the following link to register: <a href="#{registration_url}">accept invitation</a>.</p>
"""
new()
|> to(recipient(to_email, to_name))
|> from(sender())
|> subject("Invitation to #{instance_name()}")
|> html_body(html_body)
end
def account_confirmation_email(user) do
confirmation_url =
Router.Helpers.confirm_email_url(
Endpoint,
:confirm_email,
user.id,
- to_string(user.info.confirmation_token)
+ to_string(user.confirmation_token)
)
html_body = """
<h3>Welcome to #{instance_name()}!</h3>
<p>Email confirmation is required to activate the account.</p>
<p>Click the following link to proceed: <a href="#{confirmation_url}">activate your account</a>.</p>
"""
new()
|> to(recipient(user))
|> from(sender())
|> subject("#{instance_name()} account confirmation")
|> html_body(html_body)
end
@doc """
Email used in digest email notifications
Includes Mentions and New Followers data
If there are no mentions (even when new followers exist), the function will return nil
"""
@spec digest_email(User.t()) :: Swoosh.Email.t() | nil
def digest_email(user) do
notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at)
mentions =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Create"))
|> Enum.map(fn notification ->
object = Pleroma.Object.normalize(notification.activity)
object = update_in(object.data["content"], &format_links/1)
%{
data: notification,
object: object,
from: User.get_by_ap_id(notification.activity.actor)
}
end)
followers =
notifications
|> Enum.filter(&(&1.activity.data["type"] == "Follow"))
|> Enum.map(fn notification ->
%{
data: notification,
object: Pleroma.Object.normalize(notification.activity),
from: User.get_by_ap_id(notification.activity.actor)
}
end)
unless Enum.empty?(mentions) do
styling = Config.get([__MODULE__, :styling])
logo = Config.get([__MODULE__, :logo])
html_data = %{
instance: instance_name(),
user: user,
mentions: mentions,
followers: followers,
unsubscribe_link: unsubscribe_url(user, "digest"),
styling: styling
}
logo_path =
if is_nil(logo) do
Path.join(:code.priv_dir(:pleroma), "static/static/logo.png")
else
Path.join(Config.get([:instance, :static_dir]), logo)
end
new()
|> to(recipient(user))
|> from(sender())
|> subject("Your digest from #{instance_name()}")
|> put_layout(false)
|> render_body("digest.html", html_data)
|> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.png", type: :inline))
end
end
defp format_links(str) do
re = ~r/<a.+href=['"].*>/iU
%{link_color: color} = Config.get([__MODULE__, :styling])
Regex.replace(re, str, fn link ->
String.replace(link, "<a", "<a style=\"color: #{color};text-decoration: none;\"")
end)
end
@doc """
Generate unsubscribe link for given user and notifications type.
The link contains JWT token with the data, and subscription can be modified without
authorization.
"""
@spec unsubscribe_url(User.t(), String.t()) :: String.t()
def unsubscribe_url(user, notifications_type) do
token =
%{"sub" => user.id, "act" => %{"unsubscribe" => notifications_type}, "exp" => false}
|> Pleroma.JWT.generate_and_sign!()
|> Base.encode64()
Router.Helpers.subscription_url(Endpoint, :unsubscribe, token)
end
end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 931b9af2b..19b9af46c 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -1,135 +1,135 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Formatter do
alias Pleroma.HTML
alias Pleroma.User
@safe_mention_regex ~r/^(\s*(?<mentions>(@.+?\s+){1,})+)(?<rest>.*)/s
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
@auto_linker_config hashtag: true,
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
mention: true,
mention_handler: &Pleroma.Formatter.mention_handler/4
def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
case User.get_cached_by_nickname(nickname) do
%User{} ->
# escape markdown characters with `\\`
# (we don't want something like @user__name to be parsed by markdown)
String.replace(mention, @markdown_characters_regex, "\\\\\\1")
_ ->
buffer
end
end
def mention_handler("@" <> nickname, buffer, opts, acc) do
case User.get_cached_by_nickname(nickname) do
%User{id: id} = user ->
ap_id = get_ap_id(user)
nickname_text = get_nickname_text(nickname, opts)
link =
~s(<span class="h-card"><a data-user="#{id}" class="u-url mention" href="#{ap_id}" rel="ugc">@<span>#{
nickname_text
}</span></a></span>)
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
_ ->
{buffer, acc}
end
end
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag)
url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
link = ~s(<a class="hashtag" data-tag="#{tag}" href="#{url}" rel="tag ugc">#{tag_text}</a>)
{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
end
@doc """
Parses a text and replace plain text links with HTML. Returns a tuple with a result text, mentions, and hashtags.
If the 'safe_mention' option is given, only consecutive mentions at the start the post are actually mentioned.
"""
@spec linkify(String.t(), keyword()) ::
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
def linkify(text, options \\ []) do
options = options ++ @auto_linker_config
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text_mentions, %{mentions: mentions}} = AutoLinker.link_map(mentions, acc, options)
{text_rest, %{tags: tags}} = AutoLinker.link_map(rest, acc, options)
{text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
else
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
end
end
@doc """
Escapes a special characters in mention names.
"""
def mentions_escape(text, options \\ []) do
options =
Keyword.merge(options,
mention: true,
url: false,
mention_handler: &Pleroma.Formatter.escape_mention_handler/4
)
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
AutoLinker.link(mentions, options) <> AutoLinker.link(rest, options)
else
AutoLinker.link(text, options)
end
end
def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end
def html_escape(text, "text/html") do
HTML.filter_tags(text)
end
def html_escape(text, "text/plain") do
Regex.split(@link_regex, text, include_captures: true)
|> Enum.map_every(2, fn chunk ->
{:safe, part} = Phoenix.HTML.html_escape(chunk)
part
end)
|> Enum.join("")
end
def truncate(text, max_length \\ 200, omission \\ "...") do
# Remove trailing whitespace
text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}")
if String.length(text) < max_length do
text
else
length_with_omission = max_length - String.length(omission)
String.slice(text, 0, length_with_omission) <> omission
end
end
- defp get_ap_id(%User{info: %{source_data: %{"url" => url}}}) when is_binary(url), do: url
+ defp get_ap_id(%User{source_data: %{"url" => url}}) when is_binary(url), do: url
defp get_ap_id(%User{ap_id: ap_id}), do: ap_id
defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)
defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)
end
diff --git a/lib/pleroma/marker.ex b/lib/pleroma/marker.ex
new file mode 100644
index 000000000..7f87c86c3
--- /dev/null
+++ b/lib/pleroma/marker.ex
@@ -0,0 +1,74 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Marker do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Ecto.Multi
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @timelines ["notifications"]
+
+ schema "markers" do
+ field(:last_read_id, :string, default: "")
+ field(:timeline, :string, default: "")
+ field(:lock_version, :integer, default: 0)
+
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+ timestamps()
+ end
+
+ def get_markers(user, timelines \\ []) do
+ Repo.all(get_query(user, timelines))
+ end
+
+ def upsert(%User{} = user, attrs) do
+ attrs
+ |> Map.take(@timelines)
+ |> Enum.reduce(Multi.new(), fn {timeline, timeline_attrs}, multi ->
+ marker =
+ user
+ |> get_marker(timeline)
+ |> changeset(timeline_attrs)
+
+ Multi.insert(multi, timeline, marker,
+ returning: true,
+ on_conflict: {:replace, [:last_read_id]},
+ conflict_target: [:user_id, :timeline]
+ )
+ end)
+ |> Repo.transaction()
+ end
+
+ defp get_marker(user, timeline) do
+ case Repo.find_resource(get_query(user, timeline)) do
+ {:ok, marker} -> %__MODULE__{marker | user: user}
+ _ -> %__MODULE__{timeline: timeline, user_id: user.id}
+ end
+ end
+
+ @doc false
+ defp changeset(marker, attrs) do
+ marker
+ |> cast(attrs, [:last_read_id])
+ |> validate_required([:user_id, :timeline, :last_read_id])
+ |> validate_inclusion(:timeline, @timelines)
+ end
+
+ defp by_timeline(query, timeline) do
+ from(m in query, where: m.timeline in ^List.wrap(timeline))
+ end
+
+ defp by_user_id(query, id), do: from(m in query, where: m.user_id == ^id)
+
+ defp get_query(user, timelines) do
+ __MODULE__
+ |> by_user_id(user.id)
+ |> by_timeline(timelines)
+ end
+end
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
index 352cad433..e8884e6e8 100644
--- a/lib/pleroma/moderation_log.ex
+++ b/lib/pleroma/moderation_log.ex
@@ -1,554 +1,554 @@
defmodule Pleroma.ModerationLog do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.User
import Ecto.Query
schema "moderation_log" do
field(:data, :map)
timestamps()
end
def get_all(params) do
base_query =
get_all_query()
|> maybe_filter_by_date(params)
|> maybe_filter_by_user(params)
|> maybe_filter_by_search(params)
query_with_pagination = base_query |> paginate_query(params)
%{
items: Repo.all(query_with_pagination),
count: Repo.aggregate(base_query, :count, :id)
}
end
defp maybe_filter_by_date(query, %{start_date: nil, end_date: nil}), do: query
defp maybe_filter_by_date(query, %{start_date: start_date, end_date: nil}) do
from(q in query,
where: q.inserted_at >= ^parse_datetime(start_date)
)
end
defp maybe_filter_by_date(query, %{start_date: nil, end_date: end_date}) do
from(q in query,
where: q.inserted_at <= ^parse_datetime(end_date)
)
end
defp maybe_filter_by_date(query, %{start_date: start_date, end_date: end_date}) do
from(q in query,
where: q.inserted_at >= ^parse_datetime(start_date),
where: q.inserted_at <= ^parse_datetime(end_date)
)
end
defp maybe_filter_by_user(query, %{user_id: nil}), do: query
defp maybe_filter_by_user(query, %{user_id: user_id}) do
from(q in query,
where: fragment("(?)->'actor'->>'id' = ?", q.data, ^user_id)
)
end
defp maybe_filter_by_search(query, %{search: search}) when is_nil(search) or search == "",
do: query
defp maybe_filter_by_search(query, %{search: search}) do
from(q in query,
where: fragment("(?)->>'message' ILIKE ?", q.data, ^"%#{search}%")
)
end
defp paginate_query(query, %{page: page, page_size: page_size}) do
from(q in query,
limit: ^page_size,
offset: ^((page - 1) * page_size)
)
end
defp get_all_query do
from(q in __MODULE__,
order_by: [desc: q.inserted_at]
)
end
defp parse_datetime(datetime) do
{:ok, parsed_datetime, _} = DateTime.from_iso8601(datetime)
parsed_datetime
end
- @spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) ::
+ @spec insert_log(%{actor: User, subject: [User], action: String.t(), permission: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
- subject: %User{} = subject,
+ subject: subjects,
action: action,
permission: permission
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
- "subject" => user_to_map(subject),
+ "subject" => user_to_map(subjects),
"action" => action,
"permission" => permission,
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_update",
subject: %Activity{data: %{"type" => "Flag"}} = subject
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "report_update",
"subject" => report_to_map(subject),
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_response",
subject: %Activity{} = subject,
text: text
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "report_response",
"subject" => report_to_map(subject),
"text" => text,
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{
actor: User,
subject: Activity,
action: String.t(),
sensitive: String.t(),
visibility: String.t()
}) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "status_update",
subject: %Activity{} = subject,
sensitive: sensitive,
visibility: visibility
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "status_update",
"subject" => status_to_map(subject),
"sensitive" => sensitive,
"visibility" => visibility,
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "status_delete",
subject_id: subject_id
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "status_delete",
"subject_id" => subject_id,
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => action,
"subject" => user_to_map(subject),
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, subjects: [User], action: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do
subjects = Enum.map(subjects, &user_to_map/1)
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => action,
"subjects" => subjects,
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
followed: %User{} = followed,
follower: %User{} = follower,
action: "follow"
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "follow",
"followed" => user_to_map(followed),
"follower" => user_to_map(follower),
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
followed: %User{} = followed,
follower: %User{} = follower,
action: "unfollow"
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "unfollow",
"followed" => user_to_map(followed),
"follower" => user_to_map(follower),
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{
actor: User,
action: String.t(),
nicknames: [String.t()],
tags: [String.t()]
}) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
nicknames: nicknames,
tags: tags,
action: action
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"nicknames" => nicknames,
"tags" => tags,
"action" => action,
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, action: String.t(), target: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: action,
target: target
})
when action in ["relay_follow", "relay_unfollow"] do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => action,
"target" => target,
"message" => ""
}
}
|> insert_log_entry_with_message()
end
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
-
defp insert_log_entry_with_message(entry) do
entry.data["message"]
|> put_in(get_log_entry_message(entry))
|> Repo.insert()
end
+ defp user_to_map(users) when is_list(users) do
+ users |> Enum.map(&user_to_map/1)
+ end
+
defp user_to_map(%User{} = user) do
user
|> Map.from_struct()
|> Map.take([:id, :nickname])
|> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
|> Map.put("type", "user")
end
defp report_to_map(%Activity{} = report) do
%{
"type" => "report",
"id" => report.id,
"state" => report.data["state"]
}
end
defp status_to_map(%Activity{} = status) do
%{
"type" => "status",
"id" => status.id
}
end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => action,
"followed" => %{"nickname" => followed_nickname},
"follower" => %{"nickname" => follower_nickname}
}
}) do
"@#{actor_nickname} made @#{follower_nickname} #{action} @#{followed_nickname}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "delete",
- "subject" => %{"nickname" => subject_nickname, "type" => "user"}
+ "subject" => subjects
}
}) do
- "@#{actor_nickname} deleted user @#{subject_nickname}"
+ "@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "create",
"subjects" => subjects
}
}) do
- nicknames =
- subjects
- |> Enum.map(&"@#{&1["nickname"]}")
- |> Enum.join(", ")
-
- "@#{actor_nickname} created users: #{nicknames}"
+ "@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "activate",
- "subject" => %{"nickname" => subject_nickname, "type" => "user"}
+ "subject" => users
}
}) do
- "@#{actor_nickname} activated user @#{subject_nickname}"
+ "@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "deactivate",
- "subject" => %{"nickname" => subject_nickname, "type" => "user"}
+ "subject" => users
}
}) do
- "@#{actor_nickname} deactivated user @#{subject_nickname}"
+ "@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"nicknames" => nicknames,
"tags" => tags,
"action" => "tag"
}
}) do
- nicknames_string =
- nicknames
- |> Enum.map(&"@#{&1}")
- |> Enum.join(", ")
-
tags_string = tags |> Enum.join(", ")
- "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_string}"
+ "@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"nicknames" => nicknames,
"tags" => tags,
"action" => "untag"
}
}) do
- nicknames_string =
- nicknames
- |> Enum.map(&"@#{&1}")
- |> Enum.join(", ")
-
tags_string = tags |> Enum.join(", ")
- "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_string}"
+ "@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "grant",
- "subject" => %{"nickname" => subject_nickname},
+ "subject" => users,
"permission" => permission
}
}) do
- "@#{actor_nickname} made @#{subject_nickname} #{permission}"
+ "@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "revoke",
- "subject" => %{"nickname" => subject_nickname},
+ "subject" => users,
"permission" => permission
}
}) do
- "@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}"
+ "@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "relay_follow",
"target" => target
}
}) do
"@#{actor_nickname} followed relay: #{target}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "relay_unfollow",
"target" => target
}
}) do
"@#{actor_nickname} unfollowed relay: #{target}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_update",
"subject" => %{"id" => subject_id, "state" => state, "type" => "report"}
}
}) do
"@#{actor_nickname} updated report ##{subject_id} with '#{state}' state"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_response",
"subject" => %{"id" => subject_id, "type" => "report"},
"text" => text
}
}) do
"@#{actor_nickname} responded with '#{text}' to report ##{subject_id}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "status_update",
"subject" => %{"id" => subject_id, "type" => "status"},
"sensitive" => nil,
"visibility" => visibility
}
}) do
"@#{actor_nickname} updated status ##{subject_id}, set visibility: '#{visibility}'"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "status_update",
"subject" => %{"id" => subject_id, "type" => "status"},
"sensitive" => sensitive,
"visibility" => nil
}
}) do
"@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}'"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "status_update",
"subject" => %{"id" => subject_id, "type" => "status"},
"sensitive" => sensitive,
"visibility" => visibility
}
}) do
"@#{actor_nickname} updated status ##{subject_id}, set sensitive: '#{sensitive}', visibility: '#{
visibility
}'"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "status_delete",
"subject_id" => subject_id
}
}) do
"@#{actor_nickname} deleted status ##{subject_id}"
end
+
+ defp nicknames_to_string(nicknames) do
+ nicknames
+ |> Enum.map(&"@#{&1}")
+ |> Enum.join(", ")
+ end
+
+ defp users_to_nicknames_string(users) do
+ users
+ |> Enum.map(&"@#{&1["nickname"]}")
+ |> Enum.join(", ")
+ end
end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index d145f8d5b..b7ecf51e4 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -1,356 +1,361 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Notification do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
import Ecto.Query
import Ecto.Changeset
require Logger
@type t :: %__MODULE__{}
schema "notifications" do
field(:seen, :boolean, default: false)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps()
end
def changeset(%Notification{} = notification, attrs) do
notification
|> cast(attrs, [:seen])
end
def for_user_query(user, opts \\ []) do
Notification
|> where(user_id: ^user.id)
|> where(
[n, a],
fragment(
- "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
+ "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
a.actor
)
)
|> join(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object,
on:
fragment(
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
object.data,
a.data
)
)
|> preload([n, a, o], activity: {a, object: o})
|> exclude_muted(user, opts)
+ |> exclude_blocked(user)
|> exclude_visibility(opts)
end
+ defp exclude_blocked(query, user) do
+ query
+ |> where([n, a], a.actor not in ^user.blocks)
+ |> where(
+ [n, a],
+ fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
+ )
+ end
+
defp exclude_muted(query, _, %{with_muted: true}) do
query
end
defp exclude_muted(query, user, _opts) do
query
- |> where([n, a], a.actor not in ^user.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
- )
+ |> where([n, a], a.actor not in ^user.muted_notifications)
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
)
|> where([n, a, o, tm], is_nil(tm.user_id))
end
@valid_visibilities ~w[direct unlisted public private]
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
query
|> where(
[n, a],
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
query
|> where(
[n, a],
not fragment(
"activity_visibility(?, ?, ?) = (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
end
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not exclude visibility to #{visibility}")
query
end
defp exclude_visibility(query, _visibility), do: query
def for_user(user, opts \\ %{}) do
user
|> for_user_query(opts)
|> Pagination.fetch_paginated(opts)
end
@doc """
Returns notifications for user received since given date.
## Examples
iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
[%Pleroma.Notification{}, %Pleroma.Notification{}]
iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
[]
"""
@spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
def for_user_since(user, date) do
from(n in for_user_query(user),
where: n.updated_at > ^date
)
|> Repo.all()
end
def set_read_up_to(%{id: user_id} = _user, id) do
query =
from(
n in Notification,
where: n.user_id == ^user_id,
where: n.id <= ^id,
where: n.seen == false,
update: [
set: [
seen: true,
updated_at: ^NaiveDateTime.utc_now()
]
],
# Ideally we would preload object and activities here
# but Ecto does not support preloads in update_all
select: n.id
)
{_, notification_ids} = Repo.update_all(query, [])
Notification
|> where([n], n.id in ^notification_ids)
|> 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})
|> Repo.all()
end
def read_one(%User{} = user, notification_id) do
with {:ok, %Notification{} = notification} <- get(user, notification_id) do
notification
|> changeset(%{seen: true})
|> Repo.update()
end
end
def get(%{id: user_id} = _user, id) do
query =
from(
n in Notification,
where: n.id == ^id,
join: activity in assoc(n, :activity),
preload: [activity: activity]
)
notification = Repo.one(query)
case notification do
%{user_id: ^user_id} ->
{:ok, notification}
_ ->
{:error, "Cannot get notification"}
end
end
def clear(user) do
from(n in Notification, where: n.user_id == ^user.id)
|> Repo.delete_all()
end
def destroy_multiple(%{id: user_id} = _user, ids) do
from(n in Notification,
where: n.id in ^ids,
where: n.user_id == ^user_id
)
|> Repo.delete_all()
end
def dismiss(%{id: user_id} = _user, id) do
notification = Repo.get(Notification, id)
case notification do
%{user_id: ^user_id} ->
Repo.delete(notification)
_ ->
{:error, "Cannot dismiss notification"}
end
end
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
object = Object.normalize(activity)
unless object && object.data["type"] == "Answer" do
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
else
{:ok, []}
end
end
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
when type in ["Like", "Announce", "Follow"] do
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
end
def create_notifications(_), do: {:ok, []}
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do
unless skip?(activity, user) do
notification = %Notification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification)
["user", "user:notification"]
|> Streamer.stream(notification)
Push.send(notification)
notification
end
end
def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(
%Activity{data: %{"to" => _, "type" => type} = _data} = activity,
local_only
)
when type in ["Create", "Like", "Announce", "Follow"] do
recipients =
[]
|> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity)
|> Utils.maybe_notify_subscribers(activity)
|> Enum.uniq()
User.get_users_from_set(recipients, local_only)
end
def get_notified_from_activity(_, _local_only), do: []
@spec skip?(Activity.t(), User.t()) :: boolean()
def skip?(activity, user) do
[
:self,
:followers,
:follows,
:non_followers,
:non_follows,
:recently_followed
]
|> Enum.any?(&skip?(&1, activity, user))
end
@spec skip?(atom(), Activity.t(), User.t()) :: boolean()
def skip?(:self, activity, user) do
activity.data["actor"] == user.ap_id
end
def skip?(
:followers,
activity,
- %{info: %{notification_settings: %{"followers" => false}}} = user
+ %{notification_settings: %{"followers" => false}} = user
) do
actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor)
User.following?(follower, user)
end
def skip?(
:non_followers,
activity,
- %{info: %{notification_settings: %{"non_followers" => false}}} = user
+ %{notification_settings: %{"non_followers" => false}} = user
) do
actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor)
!User.following?(follower, user)
end
- def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do
+ def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = user) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed)
end
def skip?(
:non_follows,
activity,
- %{info: %{notification_settings: %{"non_follows" => false}}} = user
+ %{notification_settings: %{"non_follows" => false}} = user
) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
!User.following?(user, followed)
end
def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
actor = activity.data["actor"]
Notification.for_user(user)
|> Enum.any?(fn
%{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
_ -> false
end)
end
def skip?(_, _, _), do: false
end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index cdfbacb0e..d9b41d710 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -1,258 +1,258 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.ObjectTombstone
alias Pleroma.Repo
alias Pleroma.User
import Ecto.Query
import Ecto.Changeset
require Logger
schema "objects" do
field(:data, :map)
timestamps()
end
def create(data) do
Object.change(%Object{}, %{data: data})
|> Repo.insert()
end
def change(struct, params \\ %{}) do
struct
|> cast(params, [:data])
|> validate_required([:data])
|> unique_constraint(:ap_id, name: :objects_unique_apid_index)
end
def get_by_id(nil), do: nil
def get_by_id(id), do: Repo.get(Object, id)
def get_by_id_and_maybe_refetch(id, opts \\ []) do
%{updated_at: updated_at} = object = get_by_id(id)
if opts[:interval] &&
NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do
case Fetcher.refetch_object(object) do
{:ok, %Object{} = object} ->
object
e ->
Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}")
object
end
else
object
end
end
def get_by_ap_id(nil), do: nil
def get_by_ap_id(ap_id) do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end
defp warn_on_no_object_preloaded(ap_id) do
"Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object"
|> Logger.debug()
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
end
def normalize(_, fetch_remote \\ true, options \\ [])
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
# Use this whenever possible, especially when walking graphs in an O(N) loop!
def normalize(%Object{} = object, _, _), do: object
def normalize(%Activity{object: %Object{} = object}, _, _), do: object
# A hack for fake activities
def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _, _) do
%Object{id: "pleroma:fake_object_id", data: data}
end
# No preloaded object
def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote, _) do
warn_on_no_object_preloaded(ap_id)
normalize(ap_id, fetch_remote)
end
# No preloaded object
def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote, _) do
warn_on_no_object_preloaded(ap_id)
normalize(ap_id, fetch_remote)
end
# Old way, try fetching the object through cache.
def normalize(%{"id" => ap_id}, fetch_remote, _), do: normalize(ap_id, fetch_remote)
def normalize(ap_id, false, _) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
def normalize(ap_id, true, options) when is_binary(ap_id) do
Fetcher.fetch_object_from_id!(ap_id, options)
end
def normalize(_, _, _), do: nil
# Owned objects can only be mutated by their owner
def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
do: actor == ap_id
# Legacy objects can be mutated by anybody
def authorize_mutation(%Object{}, %User{}), do: true
def get_cached_by_ap_id(ap_id) do
key = "object:#{ap_id}"
Cachex.fetch!(:object_cache, key, fn _ ->
object = get_by_ap_id(ap_id)
if object do
{:commit, object}
else
{:ignore, object}
end
end)
end
def context_mapping(context) do
Object.change(%Object{}, %{data: %{"id" => context}})
end
def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
%ObjectTombstone{
id: id,
formerType: type,
deleted: deleted
}
|> Map.from_struct()
end
def swap_object_with_tombstone(object) do
tombstone = make_tombstone(object)
object
|> Object.change(%{data: tombstone})
|> Repo.update()
end
def delete(%Object{data: %{"id" => id}} = object) do
with {:ok, _obj} = swap_object_with_tombstone(object),
deleted_activity = Activity.delete_by_ap_id(id),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
{:ok, object, deleted_activity}
end
end
def prune(%Object{data: %{"id" => id}} = object) do
with {:ok, object} <- Repo.delete(object),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
{:ok, object}
end
end
def set_cache(%Object{data: %{"id" => ap_id}} = object) do
Cachex.put(:object_cache, "object:#{ap_id}", object)
{:ok, object}
end
def update_and_set_cache(changeset) do
with {:ok, object} <- Repo.update(changeset) do
set_cache(object)
end
end
def increase_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
- jsonb_set(?, '{repliesCount}',
+ safe_jsonb_set(?, '{repliesCount}',
(coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
def decrease_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
- jsonb_set(?, '{repliesCount}',
+ safe_jsonb_set(?, '{repliesCount}',
(greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
def increase_vote_count(ap_id, name) do
with %Object{} = object <- Object.normalize(ap_id),
"Question" <- object.data["type"] do
multiple = Map.has_key?(object.data, "anyOf")
options =
(object.data["anyOf"] || object.data["oneOf"] || [])
|> Enum.map(fn
%{"name" => ^name} = option ->
Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
option ->
option
end)
data =
if multiple do
Map.put(object.data, "anyOf", options)
else
Map.put(object.data, "oneOf", options)
end
object
|> Object.change(%{data: data})
|> update_and_set_cache()
else
_ -> :noop
end
end
@doc "Updates data field of an object"
def update_data(%Object{data: data} = object, attrs \\ %{}) do
object
|> Object.change(%{data: Map.merge(data || %{}, attrs)})
|> Repo.update()
end
end
diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex
index f077a9f32..68535c09e 100644
--- a/lib/pleroma/object/containment.ex
+++ b/lib/pleroma/object/containment.ex
@@ -1,71 +1,80 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.Containment do
@moduledoc """
This module contains some useful functions for containing objects to specific
origins and determining those origins. They previously lived in the
ActivityPub `Transmogrifier` module.
Object containment is an important step in validating remote objects to prevent
spoofing, therefore removal of object containment functions is NOT recommended.
"""
def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor
end
def get_actor(%{"actor" => actor}) when is_list(actor) do
if is_binary(Enum.at(actor, 0)) do
Enum.at(actor, 0)
else
Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
|> Map.get("id")
end
end
def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
id
end
def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
get_actor(%{"actor" => actor})
end
+ # TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus
+ # objects being present in the test suite environment. Once these objects are
+ # removed, please also remove this.
+ if Mix.env() == :test do
+ defp compare_uris(_, %URI{scheme: "tag"}), do: :ok
+ end
+
+ defp compare_uris(%URI{} = id_uri, %URI{} = other_uri) do
+ if id_uri.host == other_uri.host do
+ :ok
+ else
+ :error
+ end
+ end
+
+ defp compare_uris(_, _), do: :error
+
@doc """
Checks that an imported AP object's actor matches the domain it came from.
"""
def contain_origin(_id, %{"actor" => nil}), do: :error
def contain_origin(id, %{"actor" => _actor} = params) do
id_uri = URI.parse(id)
actor_uri = URI.parse(get_actor(params))
- if id_uri.host == actor_uri.host do
- :ok
- else
- :error
- end
+ compare_uris(actor_uri, id_uri)
end
def contain_origin(id, %{"attributedTo" => actor} = params),
do: contain_origin(id, Map.put(params, "actor", actor))
def contain_origin_from_id(_id, %{"id" => nil}), do: :error
def contain_origin_from_id(id, %{"id" => other_id} = _params) do
id_uri = URI.parse(id)
other_uri = URI.parse(other_id)
- if id_uri.host == other_uri.host do
- :ok
- else
- :error
- end
+ compare_uris(id_uri, other_uri)
end
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
do: contain_origin(id, object)
def contain_child(_), do: :ok
end
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index 5e064fd87..441ae8b65 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -1,182 +1,191 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.Fetcher do
alias Pleroma.HTTP
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
alias Pleroma.Signature
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.Transmogrifier
- alias Pleroma.Web.OStatus
require Logger
require Pleroma.Constants
defp touch_changeset(changeset) do
updated_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.truncate(:second)
Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
end
defp maybe_reinject_internal_fields(data, %{data: %{} = old_data}) do
internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
Map.merge(data, internal_fields)
end
defp maybe_reinject_internal_fields(data, _), do: data
@spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
defp reinject_object(struct, data) do
Logger.debug("Reinjecting object #{data["id"]}")
with data <- Transmogrifier.fix_object(data),
data <- maybe_reinject_internal_fields(data, struct),
changeset <- Object.change(struct, %{data: data}),
changeset <- touch_changeset(changeset),
{:ok, object} <- Repo.insert_or_update(changeset) do
{:ok, object}
else
e ->
Logger.error("Error while processing object: #{inspect(e)}")
{:error, e}
end
end
def refetch_object(%Object{data: %{"id" => id}} = object) do
with {:local, false} <- {:local, String.starts_with?(id, Pleroma.Web.base_url() <> "/")},
{:ok, data} <- fetch_and_contain_remote_object_from_id(id),
{:ok, object} <- reinject_object(object, data) do
{:ok, object}
else
{:local, true} -> object
e -> {:error, e}
end
end
# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id, options \\ []) do
with {:fetch_object, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
params <- prepare_activity_params(data),
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
- {:ok, activity} <- Transmogrifier.handle_incoming(params, options),
+ {:transmogrifier, {:ok, activity}} <-
+ {:transmogrifier, Transmogrifier.handle_incoming(params, options)},
{:object, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object}
else
{:containment, _} ->
{:error, "Object containment failed."}
- {:error, {:reject, nil}} ->
+ {:transmogrifier, {:error, {:reject, nil}}} ->
{:reject, nil}
+ {:transmogrifier, _} ->
+ {:error, "Transmogrifier failure."}
+
{:object, data, nil} ->
reinject_object(%Object{}, data)
{:normalize, object = %Object{}} ->
{:ok, object}
{:fetch_object, %Object{} = object} ->
{:ok, object}
- _e ->
- # Only fallback when receiving a fetch/normalization error with ActivityPub
- Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
+ {:fetch, {:error, error}} ->
+ {:error, error}
- # FIXME: OStatus Object Containment?
- case OStatus.fetch_activity_from_url(id) do
- {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
- e -> e
- end
+ e ->
+ e
end
end
defp prepare_activity_params(data) do
%{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
# Should we seriously keep this attributedTo thing?
"actor" => data["actor"] || data["attributedTo"],
"object" => data
}
end
def fetch_object_from_id!(id, options \\ []) do
with {:ok, object} <- fetch_object_from_id(id, options) do
object
else
- _e ->
+ {:error, %Tesla.Mock.Error{}} ->
+ nil
+
+ e ->
+ Logger.error("Error while fetching #{id}: #{inspect(e)}")
nil
end
end
defp make_signature(id, date) do
uri = URI.parse(id)
signature =
InternalFetchActor.get_actor()
|> Signature.sign(%{
"(request-target)": "get #{uri.path}",
host: uri.host,
date: date
})
[{:Signature, signature}]
end
defp sign_fetch(headers, id, date) do
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
headers ++ make_signature(id, date)
else
headers
end
end
defp maybe_date_fetch(headers, date) do
if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do
headers ++ [{:Date, date}]
else
headers
end
end
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
Logger.info("Fetching object #{id} via AP")
date = Pleroma.Signature.signed_date()
headers =
[{:Accept, "application/activity+json"}]
|> maybe_date_fetch(date)
|> sign_fetch(id, date)
Logger.debug("Fetch headers: #{inspect(headers)}")
- with true <- String.starts_with?(id, "http"),
+ with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
{:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers),
{:ok, data} <- Jason.decode(body),
:ok <- Containment.contain_origin_from_id(id, data) do
{:ok, data}
else
{:ok, %{status: code}} when code in [404, 410] ->
{:error, "Object has been deleted"}
+ {:scheme, _} ->
+ {:error, "Unsupported URI scheme"}
+
+ {:error, e} ->
+ {:error, e}
+
e ->
{:error, e}
end
end
def fetch_and_contain_remote_object_from_id(%{"id" => id}),
do: fetch_and_contain_remote_object_from_id(id)
def fetch_and_contain_remote_object_from_id(_id), do: {:error, "id must be a string"}
end
diff --git a/lib/pleroma/plugs/admin_secret_authentication_plug.ex b/lib/pleroma/plugs/admin_secret_authentication_plug.ex
index 5baf8a691..fdadd476e 100644
--- a/lib/pleroma/plugs/admin_secret_authentication_plug.ex
+++ b/lib/pleroma/plugs/admin_secret_authentication_plug.ex
@@ -1,29 +1,29 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
import Plug.Conn
alias Pleroma.User
def init(options) do
options
end
def secret_token do
Pleroma.Config.get(:admin_token)
end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(%{params: %{"admin_token" => admin_token}} = conn, _) do
if secret_token() && admin_token == secret_token() do
conn
- |> assign(:user, %User{info: %{is_admin: true}})
+ |> assign(:user, %User{is_admin: true})
else
conn
end
end
def call(conn, _), do: conn
end
diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex
index 86bc4aa3a..fd004fcd2 100644
--- a/lib/pleroma/plugs/oauth_plug.ex
+++ b/lib/pleroma/plugs/oauth_plug.ex
@@ -1,120 +1,120 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.OAuthPlug do
import Plug.Conn
import Ecto.Query
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token
@realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
def init(options), do: options
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(%{params: %{"access_token" => access_token}} = conn, _) do
with {:ok, user, token_record} <- fetch_user_and_token(access_token) do
conn
|> assign(:token, token_record)
|> assign(:user, user)
else
_ ->
# token found, but maybe only with app
with {:ok, app, token_record} <- fetch_app_and_token(access_token) do
conn
|> assign(:token, token_record)
|> assign(:app, app)
else
_ -> conn
end
end
end
def call(conn, _) do
case fetch_token_str(conn) do
{:ok, token} ->
with {:ok, user, token_record} <- fetch_user_and_token(token) do
conn
|> assign(:token, token_record)
|> assign(:user, user)
else
_ ->
# token found, but maybe only with app
with {:ok, app, token_record} <- fetch_app_and_token(token) do
conn
|> assign(:token, token_record)
|> assign(:app, app)
else
_ -> conn
end
end
_ ->
conn
end
end
# Gets user by token
#
@spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil
defp fetch_user_and_token(token) do
query =
from(t in Token,
where: t.token == ^token,
join: user in assoc(t, :user),
preload: [user: user]
)
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
- with %Token{user: %{info: %{deactivated: false} = _} = user} = token_record <- Repo.one(query) do
+ with %Token{user: %{deactivated: false} = user} = token_record <- Repo.one(query) do
{:ok, user, token_record}
end
end
@spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil
defp fetch_app_and_token(token) do
query =
from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app])
with %Token{app: app} = token_record <- Repo.one(query) do
{:ok, app, token_record}
end
end
# Gets token from session by :oauth_token key
#
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
defp fetch_token_from_session(conn) do
case get_session(conn, :oauth_token) do
nil -> :no_token_found
token -> {:ok, token}
end
end
# Gets token from headers
#
@spec fetch_token_str(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
defp fetch_token_str(%Plug.Conn{} = conn) do
headers = get_req_header(conn, "authorization")
with :no_token_found <- fetch_token_str(headers),
do: fetch_token_from_session(conn)
end
@spec fetch_token_str(Keyword.t()) :: :no_token_found | {:ok, String.t()}
defp fetch_token_str([]), do: :no_token_found
defp fetch_token_str([token | tail]) do
trimmed_token = String.trim(token)
case Regex.run(@realm_reg, trimmed_token) do
[_, match] -> {:ok, String.trim(match)}
_ -> fetch_token_str(tail)
end
end
end
diff --git a/lib/pleroma/plugs/user_enabled_plug.ex b/lib/pleroma/plugs/user_enabled_plug.ex
index da892c28b..fbb4bf115 100644
--- a/lib/pleroma/plugs/user_enabled_plug.ex
+++ b/lib/pleroma/plugs/user_enabled_plug.ex
@@ -1,21 +1,21 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserEnabledPlug do
import Plug.Conn
alias Pleroma.User
def init(options) do
options
end
- def call(%{assigns: %{user: %User{info: %{deactivated: true}}}} = conn, _) do
+ def call(%{assigns: %{user: %User{deactivated: true}}} = conn, _) do
conn
|> assign(:user, nil)
end
def call(conn, _) do
conn
end
end
diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex
index 4c4b3d610..ee808f31f 100644
--- a/lib/pleroma/plugs/user_is_admin_plug.ex
+++ b/lib/pleroma/plugs/user_is_admin_plug.ex
@@ -1,23 +1,23 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserIsAdminPlug do
import Pleroma.Web.TranslationHelpers
import Plug.Conn
alias Pleroma.User
def init(options) do
options
end
- def call(%{assigns: %{user: %User{info: %{is_admin: true}}}} = conn, _) do
+ def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do
conn
end
def call(conn, _) do
conn
|> render_error(:forbidden, "User is not admin.")
|> halt
end
end
diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index df80fbaa4..8154a09b7 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -1,85 +1,80 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Stats do
import Ecto.Query
alias Pleroma.Repo
alias Pleroma.User
use GenServer
@interval 1000 * 60 * 60
def start_link(_) do
GenServer.start_link(__MODULE__, initial_data(), name: __MODULE__)
end
def force_update do
GenServer.call(__MODULE__, :force_update)
end
def get_stats do
%{stats: stats} = GenServer.call(__MODULE__, :get_state)
stats
end
def get_peers do
%{peers: peers} = GenServer.call(__MODULE__, :get_state)
peers
end
def init(args) do
Process.send(self(), :run_update, [])
{:ok, args}
end
def handle_call(:force_update, _from, _state) do
new_stats = get_stat_data()
{:reply, new_stats, new_stats}
end
def handle_call(:get_state, _from, state) do
{:reply, state, state}
end
def handle_info(:run_update, _state) do
new_stats = get_stat_data()
Process.send_after(self(), :run_update, @interval)
{:noreply, new_stats}
end
defp initial_data do
%{peers: [], stats: %{}}
end
defp get_stat_data do
peers =
from(
u in User,
select: fragment("distinct split_part(?, '@', 2)", u.nickname),
where: u.local != ^true
)
|> Repo.all()
|> Enum.filter(& &1)
domain_count = Enum.count(peers)
- status_query =
- from(u in User.Query.build(%{local: true}),
- select: fragment("sum((?->>'note_count')::int)", u.info)
- )
-
- status_count = Repo.one(status_query)
+ status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count)
user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id)
%{
peers: peers,
stats: %{domain_count: domain_count, status_count: status_count, user_count: user_count}
}
end
end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 9f0adde5b..2e0986197 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -1,243 +1,212 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload do
@moduledoc """
Manage user uploads
Options:
* `:type`: presets for activity type (defaults to Document) and size limits from app configuration
* `:description`: upload alternative text
* `:base_url`: override base url
* `:uploader`: override uploader
* `:filters`: override filters
* `:size_limit`: override size limit
* `:activity_type`: override activity type
The `%Pleroma.Upload{}` struct: all documented fields are meant to be overwritten in filters:
* `:id` - the upload id.
* `:name` - the upload file name.
* `:path` - the upload path: set at first to `id/name` but can be changed. Keep in mind that the path
is once created permanent and changing it (especially in uploaders) is probably a bad idea!
* `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the
path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over.
Related behaviors:
* `Pleroma.Uploaders.Uploader`
* `Pleroma.Upload.Filter`
"""
alias Ecto.UUID
require Logger
@type source ::
Plug.Upload.t()
| (data_uri_string :: String.t())
| {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}
@type option ::
{:type, :avatar | :banner | :background}
| {:description, String.t()}
| {:activity_type, String.t()}
| {:size_limit, nil | non_neg_integer()}
| {:uploader, module()}
| {:filters, [module()]}
@type t :: %__MODULE__{
id: String.t(),
name: String.t(),
tempfile: String.t(),
content_type: String.t(),
path: String.t()
}
defstruct [:id, :name, :tempfile, :content_type, :path]
@spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
def store(upload, opts \\ []) do
opts = get_opts(opts)
with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok,
%{
"type" => opts.activity_type,
"url" => [
%{
"type" => "Link",
"mediaType" => upload.content_type,
"href" => url_from_spec(upload, opts.base_url, url_spec)
}
],
"name" => Map.get(opts, :description) || upload.name
}}
else
{:error, error} ->
Logger.error(
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
)
{:error, error}
end
end
def char_unescaped?(char) do
URI.char_unreserved?(char) or char == ?/
end
defp get_opts(opts) do
{size_limit, activity_type} =
case Keyword.get(opts, :type) do
:banner ->
{Pleroma.Config.get!([:instance, :banner_upload_limit]), "Image"}
:avatar ->
{Pleroma.Config.get!([:instance, :avatar_upload_limit]), "Image"}
:background ->
{Pleroma.Config.get!([:instance, :background_upload_limit]), "Image"}
_ ->
{Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
end
- opts = %{
+ %{
activity_type: Keyword.get(opts, :activity_type, activity_type),
size_limit: Keyword.get(opts, :size_limit, size_limit),
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
filters: Keyword.get(opts, :filters, Pleroma.Config.get([__MODULE__, :filters])),
description: Keyword.get(opts, :description),
base_url:
Keyword.get(
opts,
: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
defp prepare_upload(%Plug.Upload{} = file, opts) do
with :ok <- check_file_size(file.path, opts.size_limit),
{:ok, content_type, name} <- Pleroma.MIME.file_mime_type(file.path, file.filename) do
{:ok,
%__MODULE__{
id: UUID.generate(),
name: name,
tempfile: file.path,
content_type: content_type
}}
end
end
defp prepare_upload(%{"img" => "data:image/" <> image_data}, opts) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace)
hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data)))
with :ok <- check_binary_size(data, opts.size_limit),
tmp_path <- tempfile_for_image(data),
{:ok, content_type, name} <-
Pleroma.MIME.bin_mime_type(data, hash <> "." <> parsed["filetype"]) do
{:ok,
%__MODULE__{
id: UUID.generate(),
name: name,
tempfile: tmp_path,
content_type: content_type
}}
end
end
# For Mix.Tasks.MigrateLocalUploads
defp prepare_upload(%__MODULE__{tempfile: path} = upload, _opts) do
with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do
{:ok, %__MODULE__{upload | content_type: content_type}}
end
end
defp check_binary_size(binary, size_limit)
when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do
{:error, :file_too_large}
end
defp check_binary_size(_, _), do: :ok
defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do
with {:ok, %{size: size}} <- File.stat(path),
true <- size <= size_limit do
:ok
else
false -> {:error, :file_too_large}
error -> error
end
end
defp check_file_size(_, _), do: :ok
# Creates a tempfile using the Plug.Upload Genserver which cleans them up
# automatically.
defp tempfile_for_image(data) do
{:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
{:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
IO.binwrite(tmp_file, data)
tmp_path
end
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
path =
URI.encode(path, &char_unescaped?/1) <>
if Pleroma.Config.get([__MODULE__, :link_name], false) do
"?name=#{URI.encode(name, &char_unescaped?/1)}"
else
""
end
prefix =
if is_nil(Pleroma.Config.get([__MODULE__, :base_url])) do
"media"
else
""
end
[base_url, prefix, path]
|> Path.join()
end
defp url_from_spec(_upload, _base_url, {:url, url}), do: url
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 2cfb13a8c..5d3f55721 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1,1633 +1,1988 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Comeonin.Pbkdf2
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
alias Pleroma.Keys
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.RepoStreamer
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.OAuth
- alias Pleroma.Web.OStatus
alias Pleroma.Web.RelMe
- alias Pleroma.Web.Websub
alias Pleroma.Workers.BackgroundWorker
require Logger
@type t :: %__MODULE__{}
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
@strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
@extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
schema "users" do
field(:bio, :string)
field(:email, :string)
field(:name, :string)
field(:nickname, :string)
field(:password_hash, :string)
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
field(:keys, :string)
field(:following, {:array, :string}, default: [])
field(:ap_id, :string)
field(:avatar, :map)
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:following_address, :string)
field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec)
field(:last_digest_emailed_at, :naive_datetime)
+
+ field(:banner, :map, default: %{})
+ field(:background, :map, default: %{})
+ field(:source_data, :map, default: %{})
+ field(:note_count, :integer, default: 0)
+ field(:follower_count, :integer, default: 0)
+ # Should be filled in only for remote users
+ field(:following_count, :integer, default: nil)
+ field(:locked, :boolean, default: false)
+ field(:confirmation_pending, :boolean, default: false)
+ field(:password_reset_pending, :boolean, default: false)
+ field(:confirmation_token, :string, default: nil)
+ field(:default_scope, :string, default: "public")
+ field(:blocks, {:array, :string}, default: [])
+ field(:domain_blocks, {:array, :string}, default: [])
+ field(:mutes, {:array, :string}, default: [])
+ field(:muted_reblogs, {:array, :string}, default: [])
+ field(:muted_notifications, {:array, :string}, default: [])
+ field(:subscribers, {:array, :string}, default: [])
+ field(:deactivated, :boolean, default: false)
+ field(:no_rich_text, :boolean, default: false)
+ field(:ap_enabled, :boolean, default: false)
+ field(:is_moderator, :boolean, default: false)
+ field(:is_admin, :boolean, default: false)
+ field(:show_role, :boolean, default: true)
+ field(:settings, :map, default: nil)
+ field(:magic_key, :string, default: nil)
+ field(:uri, :string, default: nil)
+ field(:hide_followers_count, :boolean, default: false)
+ field(:hide_follows_count, :boolean, default: false)
+ field(:hide_followers, :boolean, default: false)
+ field(:hide_follows, :boolean, default: false)
+ field(:hide_favorites, :boolean, default: true)
+ field(:unread_conversation_count, :integer, default: 0)
+ field(:pinned_activities, {:array, :string}, default: [])
+ field(:email_notifications, :map, default: %{"digest" => false})
+ field(:mascot, :map, default: nil)
+ field(:emoji, {:array, :map}, default: [])
+ field(:pleroma_settings_store, :map, default: %{})
+ field(:fields, {:array, :map}, default: [])
+ field(:raw_fields, {:array, :map}, default: [])
+ field(:discoverable, :boolean, default: false)
+ field(:invisible, :boolean, default: false)
+ field(:skip_thread_containment, :boolean, default: false)
+
+ field(:notification_settings, :map,
+ default: %{
+ "followers" => true,
+ "follows" => true,
+ "non_follows" => true,
+ "non_followers" => true
+ }
+ )
+
has_many(:notifications, Notification)
has_many(:registrations, Registration)
has_many(:deliveries, Delivery)
- embeds_one(:info, User.Info)
+
+ field(:info, :map, default: %{})
timestamps()
end
- def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
+ def auth_active?(%User{confirmation_pending: true}),
do: !Pleroma.Config.get([:instance, :account_activation_required])
def auth_active?(%User{}), do: true
def visible_for?(user, for_user \\ nil)
def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
def visible_for?(%User{} = user, for_user) do
auth_active?(user) || superuser?(for_user)
end
def visible_for?(_, _), do: false
- def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
- def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
+ def superuser?(%User{local: true, is_admin: true}), do: true
+ def superuser?(%User{local: true, is_moderator: true}), do: true
def superuser?(_), do: false
+ def invisible?(%User{invisible: true}), do: true
+ def invisible?(_), do: false
+
def avatar_url(user, options \\ []) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href
_ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
end
end
def banner_url(user, options \\ []) do
- case user.info.banner do
+ case user.banner do
%{"url" => [%{"href" => href} | _]} -> href
_ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
end
end
- def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
+ def profile_url(%User{source_data: %{"url" => url}}), do: url
def profile_url(%User{ap_id: ap_id}), do: ap_id
def profile_url(_), do: nil
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
@spec ap_following(User.t()) :: Sring.t()
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
def user_info(%User{} = user, args \\ %{}) do
following_count =
- Map.get(args, :following_count, user.info.following_count || following_count(user))
+ Map.get(args, :following_count, user.following_count || following_count(user))
- follower_count = Map.get(args, :follower_count, user.info.follower_count)
+ follower_count = Map.get(args, :follower_count, user.follower_count)
%{
- note_count: user.info.note_count,
- locked: user.info.locked,
- confirmation_pending: user.info.confirmation_pending,
- default_scope: user.info.default_scope
+ note_count: user.note_count,
+ locked: user.locked,
+ confirmation_pending: user.confirmation_pending,
+ default_scope: user.default_scope
}
|> Map.put(:following_count, following_count)
|> Map.put(:follower_count, follower_count)
end
def follow_state(%User{} = user, %User{} = target) do
case Utils.fetch_latest_follow(user, target) do
%{data: %{"state" => state}} -> state
# Ideally this would be nil, but then Cachex does not commit the value
_ -> false
end
end
def get_cached_follow_state(user, target) do
key = "follow_state:#{user.ap_id}|#{target.ap_id}"
Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
end
@spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
def set_follow_state_cache(user_ap_id, target_ap_id, state) do
Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
end
def set_info_cache(user, args) do
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
end
@spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
def restrict_deactivated(query) do
- from(u in query,
- where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
- )
+ from(u in query, where: u.deactivated != ^true)
end
def following_count(%User{following: []}), do: 0
def following_count(%User{} = user) do
user
|> get_friends_query()
|> Repo.aggregate(:count, :id)
end
+ defp truncate_fields_param(params) do
+ if Map.has_key?(params, :fields) do
+ Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
+ else
+ params
+ end
+ end
+
defp truncate_if_exists(params, key, max_length) do
if Map.has_key?(params, key) and is_binary(params[key]) do
{value, _chopped} = String.split_at(params[key], max_length)
Map.put(params, key, value)
else
params
end
end
def remote_user_creation(params) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
params =
params
|> Map.put(:info, params[:info] || %{})
|> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit)
+ |> truncate_fields_param()
changeset =
%User{local: false}
- |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
+ |> cast(
+ params,
+ [
+ :bio,
+ :name,
+ :ap_id,
+ :nickname,
+ :avatar,
+ :ap_enabled,
+ :source_data,
+ :banner,
+ :locked,
+ :magic_key,
+ :uri,
+ :hide_followers,
+ :hide_follows,
+ :hide_followers_count,
+ :hide_follows_count,
+ :follower_count,
+ :fields,
+ :following_count,
+ :discoverable,
+ :invisible
+ ]
+ )
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
- |> change_info(&User.Info.remote_user_creation(&1, params[:info]))
+ |> validate_fields(true)
- case params[:info][:source_data] do
+ case params[:source_data] do
%{"followers" => followers, "following" => following} ->
changeset
|> put_change(:follower_address, followers)
|> put_change(:following_address, following)
_ ->
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :follower_address, followers)
end
end
def update_changeset(struct, params \\ %{}) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
struct
- |> cast(params, [:bio, :name, :avatar, :following])
+ |> cast(
+ params,
+ [
+ :bio,
+ :name,
+ :avatar,
+ :following,
+ :locked,
+ :no_rich_text,
+ :default_scope,
+ :banner,
+ :hide_follows,
+ :hide_followers,
+ :hide_followers_count,
+ :hide_follows_count,
+ :hide_favorites,
+ :background,
+ :show_role,
+ :skip_thread_containment,
+ :fields,
+ :raw_fields,
+ :pleroma_settings_store,
+ :discoverable
+ ]
+ )
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
+ |> validate_fields(false)
end
def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
+ params = if remote?, do: truncate_fields_param(params), else: params
+
struct
- |> cast(params, [
- :bio,
- :name,
- :follower_address,
- :following_address,
- :avatar,
- :last_refreshed_at
- ])
+ |> cast(
+ params,
+ [
+ :bio,
+ :name,
+ :follower_address,
+ :following_address,
+ :avatar,
+ :last_refreshed_at,
+ :ap_enabled,
+ :source_data,
+ :banner,
+ :locked,
+ :magic_key,
+ :follower_count,
+ :following_count,
+ :hide_follows,
+ :fields,
+ :hide_followers,
+ :discoverable,
+ :hide_followers_count,
+ :hide_follows_count
+ ]
+ )
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
- |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
+ |> validate_fields(remote?)
end
def password_update_changeset(struct, params) do
struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
- |> put_password_hash
- |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
+ |> put_password_hash()
+ |> put_change(:password_reset_pending, false)
end
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def reset_password(%User{id: user_id} = user, data) do
multi =
Multi.new()
|> Multi.update(:user, password_update_changeset(user, data))
|> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
|> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
case Repo.transaction(multi) do
{:ok, %{user: user} = _} -> set_cache(user)
{:error, _, changeset, _} -> {:error, changeset}
end
end
+ def update_password_reset_pending(user, value) do
+ user
+ |> change()
+ |> put_change(:password_reset_pending, value)
+ |> update_and_set_cache()
+ end
+
def force_password_reset_async(user) do
BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
end
@spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
- def force_password_reset(user) do
- info_cng = User.Info.set_password_reset_pending(user.info, true)
-
- user
- |> change()
- |> put_embed(:info, info_cng)
- |> update_and_set_cache()
- end
+ def force_password_reset(user), do: update_password_reset_pending(user, true)
def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
need_confirmation? =
if is_nil(opts[:need_confirmation]) do
Pleroma.Config.get([:instance, :account_activation_required])
else
opts[:need_confirmation]
end
struct
+ |> confirmation_changeset(need_confirmation: need_confirmation?)
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
- |> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
|> maybe_validate_required_email(opts[:external])
|> put_password_hash
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_address()
end
def maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
defp put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id)
end
defp put_following_and_follower_address(changeset) do
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
changeset
|> put_change(:following, [followers])
|> put_change(:follower_address, followers)
end
defp autofollow_users(user) do
candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
autofollowed_users =
User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|> Repo.all()
follow_all(user, autofollowed_users)
end
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset) do
post_register_action(user)
end
end
def post_register_action(%User{} = user) do
with {:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user),
{:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
end
end
def try_send_confirmation_email(%User{} = user) do
- if user.info.confirmation_pending &&
+ if user.confirmation_pending &&
Pleroma.Config.get([:instance, :account_activation_required]) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async()
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def needs_update?(%User{local: true}), do: false
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
def needs_update?(%User{local: false} = user) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
end
def needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
- def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
+ def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true}) do
{:ok, follower}
end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
follow(follower, followed)
end
def maybe_direct_follow(%User{} = follower, %User{} = followed) do
if not ap_enabled?(followed) do
follow(follower, followed)
else
{:ok, follower}
end
end
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do
followed_addresses =
followeds
|> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
|> Enum.map(fn %{follower_address: fa} -> fa end)
q =
from(u in User,
where: u.id == ^follower.id,
update: [
set: [
following:
fragment(
"array(select distinct unnest (array_cat(?, ?)))",
u.following,
^followed_addresses
)
]
],
select: u
)
{1, [follower]} = Repo.update_all(q, [])
Enum.each(followeds, &update_follower_count/1)
set_cache(follower)
end
- def follow(%User{} = follower, %User{info: info} = followed) do
+ def follow(%User{} = follower, %User{} = followed) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
ap_followers = followed.follower_address
cond do
- info.deactivated ->
- {:error, "Could not follow user: You are deactivated."}
+ followed.deactivated ->
+ {:error, "Could not follow user: #{followed.nickname} is deactivated."}
deny_follow_blocked and blocks?(followed, follower) ->
{:error, "Could not follow user: #{followed.nickname} blocked you."}
true ->
- if !followed.local && follower.local && !ap_enabled?(followed) do
- Websub.subscribe(follower, followed)
- end
-
q =
from(u in User,
where: u.id == ^follower.id,
update: [push: [following: ^ap_followers]],
select: u
)
{1, [follower]} = Repo.update_all(q, [])
follower = maybe_update_following_count(follower)
{:ok, _} = update_follower_count(followed)
set_cache(follower)
end
end
def unfollow(%User{} = follower, %User{} = followed) do
ap_followers = followed.follower_address
if following?(follower, followed) and follower.ap_id != followed.ap_id do
q =
from(u in User,
where: u.id == ^follower.id,
update: [pull: [following: ^ap_followers]],
select: u
)
{1, [follower]} = Repo.update_all(q, [])
follower = maybe_update_following_count(follower)
{:ok, followed} = update_follower_count(followed)
set_cache(follower)
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
else
{:error, "Not subscribed!"}
end
end
@spec following?(User.t(), User.t()) :: boolean
def following?(%User{} = follower, %User{} = followed) do
Enum.member?(follower.following, followed.follower_address)
end
def locked?(%User{} = user) do
- user.info.locked || false
+ user.locked || false
end
def get_by_id(id) do
Repo.get_by(User, id: id)
end
def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id)
end
def get_all_by_ap_id(ap_ids) do
from(u in __MODULE__,
where: u.ap_id in ^ap_ids
)
|> Repo.all()
end
def get_all_by_ids(ids) do
from(u in __MODULE__, where: u.id in ^ids)
|> Repo.all()
end
# This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
# of the ap_id and the domain and tries to get that user
def get_by_guessed_nickname(ap_id) do
domain = URI.parse(ap_id).host
name = List.last(String.split(ap_id, "/"))
nickname = "#{name}@#{domain}"
get_cached_by_nickname(nickname)
end
def set_cache({:ok, user}), do: set_cache(user)
def set_cache({:error, err}), do: {:error, err}
def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
{:ok, user}
end
+ def update_and_set_cache(struct, params) do
+ struct
+ |> update_changeset(params)
+ |> update_and_set_cache()
+ end
+
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
set_cache(user)
end
end
def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}")
Cachex.del(:user_cache, "user_info:#{user.id}")
end
def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}"
Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
end
def get_cached_by_id(id) do
key = "id:#{id}"
ap_id =
Cachex.fetch!(:user_cache, key, fn _ ->
user = get_by_id(id)
if user do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
{:commit, user.ap_id}
else
{:ignore, ""}
end
end)
get_cached_by_ap_id(ap_id)
end
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn ->
case get_or_fetch_by_nickname(nickname) do
{:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil}
end
end)
end
def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
cond do
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
get_cached_by_nickname(nickname_or_id)
restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
get_cached_by_nickname(nickname_or_id)
true ->
nil
end
end
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) ||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
Repo.get_by(User, nickname: local_nickname(nickname))
end
end
def get_by_email(email), do: Repo.get_by(User, email: email)
def get_by_nickname_or_email(nickname_or_email) do
get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
end
def get_cached_user_info(user) do
key = "user_info:#{user.id}"
Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
end
- def fetch_by_nickname(nickname) do
- case ActivityPub.make_user_from_nickname(nickname) do
- {:ok, user} -> {:ok, user}
- _ -> OStatus.make_user(nickname)
- end
- end
+ def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do
{:ok, user}
else
_e ->
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
fetch_initial_posts(user)
end
{:ok, user}
else
_e -> {:error, "not found " <> nickname}
end
end
end
@doc "Fetch some posts when the user has just been federated with"
def fetch_initial_posts(user) do
BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
end
@spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_followers_query(%User{} = user, nil) do
User.Query.build(%{followers: user, deactivated: false})
end
def get_followers_query(user, page) do
user
|> get_followers_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_followers_query(User.t()) :: Ecto.Query.t()
def get_followers_query(user), do: get_followers_query(user, nil)
@spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
def get_followers(user, page \\ nil) do
user
|> get_followers_query(page)
|> Repo.all()
end
@spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
def get_external_followers(user, page \\ nil) do
user
|> get_followers_query(page)
|> User.Query.build(%{external: true})
|> Repo.all()
end
def get_followers_ids(user, page \\ nil) do
user
|> get_followers_query(page)
|> select([u], u.id)
|> Repo.all()
end
@spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_friends_query(%User{} = user, nil) do
User.Query.build(%{friends: user, deactivated: false})
end
def get_friends_query(user, page) do
user
|> get_friends_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_friends_query(User.t()) :: Ecto.Query.t()
def get_friends_query(user), do: get_friends_query(user, nil)
def get_friends(user, page \\ nil) do
user
|> get_friends_query(page)
|> Repo.all()
end
def get_friends_ids(user, page \\ nil) do
user
|> get_friends_query(page)
|> select([u], u.id)
|> Repo.all()
end
@spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
def get_follow_requests(%User{} = user) do
user
|> Activity.follow_requests_for_actor()
|> join(:inner, [a], u in User, on: a.actor == u.ap_id)
|> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
|> group_by([a, u], u.id)
|> select([a, u], u)
|> Repo.all()
end
def increase_note_count(%User{} = user) do
User
|> where(id: ^user.id)
- |> update([u],
- set: [
- info:
- fragment(
- "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
- u.info,
- u.info
- )
- ]
- )
+ |> update([u], inc: [note_count: 1])
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def decrease_note_count(%User{} = user) do
User
|> where(id: ^user.id)
|> update([u],
set: [
- info:
- fragment(
- "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
- u.info,
- u.info
- )
+ note_count: fragment("greatest(0, note_count - 1)")
]
)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
- def update_note_count(%User{} = user) do
+ def update_note_count(%User{} = user, note_count \\ nil) do
note_count =
- from(
- a in Object,
- where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
- select: count(a.id)
- )
- |> Repo.one()
-
- update_info(user, &User.Info.set_note_count(&1, note_count))
- end
-
- def update_mascot(user, url) do
- info_changeset =
- User.Info.mascot_update(
- user.info,
- url
- )
+ note_count ||
+ from(
+ a in Object,
+ where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
+ select: count(a.id)
+ )
+ |> Repo.one()
user
- |> change()
- |> put_embed(:info, info_changeset)
+ |> cast(%{note_count: note_count}, [:note_count])
|> update_and_set_cache()
end
@spec maybe_fetch_follow_information(User.t()) :: User.t()
def maybe_fetch_follow_information(user) do
with {:ok, user} <- fetch_follow_information(user) do
user
else
e ->
Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
user
end
end
def fetch_follow_information(user) do
with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
- update_info(user, &User.Info.follow_information_update(&1, info))
+ user
+ |> follow_information_changeset(info)
+ |> update_and_set_cache()
end
end
+ defp follow_information_changeset(user, params) do
+ user
+ |> cast(params, [
+ :hide_followers,
+ :hide_follows,
+ :follower_count,
+ :following_count,
+ :hide_followers_count,
+ :hide_follows_count
+ ])
+ end
+
def update_follower_count(%User{} = user) do
if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
follower_count_query =
User.Query.build(%{followers: user, deactivated: false})
|> select([u], %{count: count(u.id)})
User
|> where(id: ^user.id)
|> join(:inner, [u], s in subquery(follower_count_query))
|> update([u, s],
- set: [
- info:
- fragment(
- "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
- u.info,
- s.count
- )
- ]
+ set: [follower_count: s.count]
)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
else
{:ok, maybe_fetch_follow_information(user)}
end
end
@spec maybe_update_following_count(User.t()) :: User.t()
def maybe_update_following_count(%User{local: false} = user) do
if Pleroma.Config.get([:instance, :external_user_synchronization]) do
maybe_fetch_follow_information(user)
else
user
end
end
def maybe_update_following_count(user), do: user
def set_unread_conversation_count(%User{local: true} = user) do
unread_query = Participation.unread_conversation_count_for_user(user)
User
|> join(:inner, [u], p in subquery(unread_query))
|> update([u, p],
- set: [
- info:
- fragment(
- "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
- u.info,
- p.count
- )
- ]
+ set: [unread_conversation_count: p.count]
)
|> where([u], u.id == ^user.id)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
- def set_unread_conversation_count(_), do: :noop
+ def set_unread_conversation_count(user), do: {:ok, user}
def increment_unread_conversation_count(conversation, %User{local: true} = user) do
unread_query =
Participation.unread_conversation_count_for_user(user)
|> where([p], p.conversation_id == ^conversation.id)
User
|> join(:inner, [u], p in subquery(unread_query))
|> update([u, p],
- set: [
- info:
- fragment(
- "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
- u.info,
- u.info
- )
- ]
+ inc: [unread_conversation_count: 1]
)
|> where([u], u.id == ^user.id)
|> where([u, p], p.count == 0)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
- def increment_unread_conversation_count(_, _), do: :noop
+ def increment_unread_conversation_count(_, user), do: {:ok, user}
def remove_duplicated_following(%User{following: following} = user) do
uniq_following = Enum.uniq(following)
if length(following) == length(uniq_following) do
{:ok, user}
else
user
|> update_changeset(%{following: uniq_following})
|> update_and_set_cache()
end
end
@spec get_users_from_set([String.t()], boolean()) :: [User.t()]
def get_users_from_set(ap_ids, local_only \\ true) do
criteria = %{ap_id: ap_ids, deactivated: false}
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
User.Query.build(criteria)
|> Repo.all()
end
@spec get_recipients_from_activity(Activity.t()) :: [User.t()]
def get_recipients_from_activity(%Activity{recipients: to}) do
User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
|> Repo.all()
end
@spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
- update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
+ add_to_mutes(muter, ap_id, notifications?)
end
def unmute(muter, %{ap_id: ap_id}) do
- update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
+ remove_from_mutes(muter, ap_id)
end
def subscribe(subscriber, %{ap_id: ap_id}) do
with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
if blocks?(subscribed, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
else
- update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
+ User.add_to_subscribers(subscribed, subscriber.ap_id)
end
end
end
def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
with %User{} = user <- get_cached_by_ap_id(ap_id) do
- update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
+ User.remove_from_subscribers(user, unsubscriber.ap_id)
end
end
def block(blocker, %User{ap_id: ap_id} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
if following?(blocker, blocked) do
{:ok, blocker, _} = unfollow(blocker, blocked)
blocker
else
blocker
end
# clear any requested follows as well
blocked =
case CommonAPI.reject_follow_request(blocked, blocker) do
{:ok, %User{} = updated_blocked} -> updated_blocked
nil -> blocked
end
blocker =
if subscribed_to?(blocked, blocker) do
{:ok, blocker} = unsubscribe(blocked, blocker)
blocker
else
blocker
end
if following?(blocked, blocker), do: unfollow(blocked, blocker)
{:ok, blocker} = update_follower_count(blocker)
-
- update_info(blocker, &User.Info.add_to_block(&1, ap_id))
+ {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
+ add_to_block(blocker, ap_id)
end
# helper to handle the block given only an actor's AP id
def block(blocker, %{ap_id: ap_id}) do
block(blocker, get_cached_by_ap_id(ap_id))
end
def unblock(blocker, %{ap_id: ap_id}) do
- update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
+ remove_from_block(blocker, ap_id)
end
def mutes?(nil, _), do: false
- def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
+ def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id)
@spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
def muted_notifications?(nil, _), do: false
def muted_notifications?(user, %{ap_id: ap_id}),
- do: Enum.member?(user.info.muted_notifications, ap_id)
+ do: Enum.member?(user.muted_notifications, ap_id)
def blocks?(%User{} = user, %User{} = target) do
blocks_ap_id?(user, target) || blocks_domain?(user, target)
end
def blocks?(nil, _), do: false
def blocks_ap_id?(%User{} = user, %User{} = target) do
- Enum.member?(user.info.blocks, target.ap_id)
+ Enum.member?(user.blocks, target.ap_id)
end
def blocks_ap_id?(_, _), do: false
def blocks_domain?(%User{} = user, %User{} = target) do
- domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
+ domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
%{host: host} = URI.parse(target.ap_id)
Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
end
def blocks_domain?(_, _), do: false
def subscribed_to?(user, %{ap_id: ap_id}) do
with %User{} = target <- get_cached_by_ap_id(ap_id) do
- Enum.member?(target.info.subscribers, user.ap_id)
+ Enum.member?(target.subscribers, user.ap_id)
end
end
@spec muted_users(User.t()) :: [User.t()]
def muted_users(user) do
- User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
+ User.Query.build(%{ap_id: user.mutes, deactivated: false})
|> Repo.all()
end
@spec blocked_users(User.t()) :: [User.t()]
def blocked_users(user) do
- User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
+ User.Query.build(%{ap_id: user.blocks, deactivated: false})
|> Repo.all()
end
@spec subscribers(User.t()) :: [User.t()]
def subscribers(user) do
- User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
+ User.Query.build(%{ap_id: user.subscribers, deactivated: false})
|> Repo.all()
end
- def block_domain(user, domain) do
- update_info(user, &User.Info.add_to_domain_block(&1, domain))
+ def deactivate_async(user, status \\ true) do
+ BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
end
- def unblock_domain(user, domain) do
- update_info(user, &User.Info.remove_from_domain_block(&1, domain))
- end
+ def deactivate(user, status \\ true)
- def deactivate_async(user, status \\ true) do
- BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
+ 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 \\ true) do
- with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
+ def deactivate(%User{} = user, status) do
+ with {:ok, user} <- set_activation_status(user, status) do
Enum.each(get_followers(user), &invalidate_cache/1)
Enum.each(get_friends(user), &update_follower_count/1)
{:ok, user}
end
end
- def update_notification_settings(%User{} = user, settings \\ %{}) do
- update_info(user, &User.Info.update_notification_settings(&1, settings))
+ def update_notification_settings(%User{} = user, settings) do
+ settings =
+ settings
+ |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
+ |> Map.new()
+
+ notification_settings =
+ user.notification_settings
+ |> Map.merge(settings)
+ |> Map.take(["followers", "follows", "non_follows", "non_followers"])
+
+ params = %{notification_settings: notification_settings}
+
+ user
+ |> cast(params, [:notification_settings])
+ |> validate_required([:notification_settings])
+ |> update_and_set_cache()
+ end
+
+ def delete(users) when is_list(users) do
+ for user <- users, do: delete(user)
end
def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
def perform(:force_password_reset, user), do: force_password_reset(user)
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
{:ok, _user} = ActivityPub.delete(user)
# Remove all relationships
user
|> get_followers()
|> Enum.each(fn follower ->
ActivityPub.unfollow(follower, user)
unfollow(follower, user)
end)
user
|> get_friends()
|> Enum.each(fn followed ->
ActivityPub.unfollow(user, followed)
unfollow(user, followed)
end)
delete_user_activities(user)
invalidate_cache(user)
Repo.delete(user)
end
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:fetch_initial_posts, %User{} = user) do
pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
# Insert all the posts in reverse order, so they're in the right order on the timeline
- user.info.source_data["outbox"]
+ user.source_data["outbox"]
|> Utils.fetch_ordered_collection(pages)
|> Enum.reverse()
|> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
end
def perform(:deactivate_async, user, status), do: deactivate(user, status)
@spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
when is_list(blocked_identifiers) do
Enum.map(
blocked_identifiers,
fn blocked_identifier ->
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, blocker} <- block(blocker, blocked),
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked
else
err ->
Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
err
end
end
)
end
@spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
def perform(:follow_import, %User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
Enum.map(
followed_identifiers,
fn followed_identifier ->
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do
followed
else
err ->
Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
err
end
end
)
end
@spec external_users_query() :: Ecto.Query.t()
def external_users_query do
User.Query.build(%{
external: true,
active: true,
order_by: :id
})
end
@spec external_users(keyword()) :: [User.t()]
def external_users(opts \\ []) do
query =
external_users_query()
|> select([u], struct(u, [:id, :ap_id, :info]))
query =
if opts[:max_id],
do: where(query, [u], u.id > ^opts[:max_id]),
else: query
query =
if opts[:limit],
do: limit(query, ^opts[:limit]),
else: query
Repo.all(query)
end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
BackgroundWorker.enqueue("blocks_import", %{
"blocker_id" => blocker.id,
"blocked_identifiers" => blocked_identifiers
})
end
def follow_import(%User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
BackgroundWorker.enqueue("follow_import", %{
"follower_id" => follower.id,
"followed_identifiers" => followed_identifiers
})
end
def delete_user_activities(%User{ap_id: ap_id}) do
ap_id
|> Activity.Queries.by_actor()
|> RepoStreamer.chunk_stream(50)
|> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
|> Stream.run()
end
defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
activity
|> Object.normalize()
|> ActivityPub.delete()
end
defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
object = Object.normalize(activity)
activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unlike(object)
end
defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
object = Object.normalize(activity)
activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unannounce(object)
end
defp delete_activity(_activity), do: "Doing nothing"
- def html_filter_policy(%User{info: %{no_rich_text: true}}) do
+ def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText
end
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
- def fetch_by_ap_id(ap_id) do
- case ActivityPub.make_user_from_ap_id(ap_id) do
- {:ok, user} ->
- {:ok, user}
-
- _ ->
- case OStatus.make_user(ap_id) do
- {:ok, user} -> {:ok, user}
- _ -> {:error, "Could not fetch by AP id"}
- end
- end
- end
+ def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
def get_or_fetch_by_ap_id(ap_id) do
user = get_cached_by_ap_id(ap_id)
if !is_nil(user) and !needs_update?(user) do
{:ok, user}
else
# Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
resp = fetch_by_ap_id(ap_id)
if should_fetch_initial do
with {:ok, %User{} = user} <- resp do
fetch_initial_posts(user)
end
end
resp
end
end
@doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
with %User{} = user <- get_cached_by_ap_id(uri) do
user
else
_ ->
{:ok, user} =
- %User{info: %User.Info{}}
+ %User{}
|> cast(%{}, [:ap_id, :nickname, :local])
|> put_change(:ap_id, uri)
|> put_change(:nickname, nickname)
|> put_change(:local, true)
|> put_change(:follower_address, uri <> "/followers")
|> Repo.insert()
user
end
end
# AP style
- def public_key_from_info(%{
- source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
- }) do
+ def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
key =
public_key_pem
|> :public_key.pem_decode()
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
end
- # OStatus Magic Key
- def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
- {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
- end
-
- def public_key_from_info(_), do: {:error, "not found key"}
+ def public_key(_), do: {:error, "not found key"}
def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
- {:ok, public_key} <- public_key_from_info(user.info) do
+ {:ok, public_key} <- public_key(user) do
{:ok, public_key}
else
_ -> :error
end
end
defp blank?(""), do: nil
defp blank?(n), do: n
def insert_or_update_user(data) do
data
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
|> remote_user_creation()
|> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
|> set_cache()
end
def ap_enabled?(%User{local: true}), do: true
- def ap_enabled?(%User{info: info}), do: info.ap_enabled
+ def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
def ap_enabled?(_), do: false
@doc "Gets or fetch a user by uri or nickname."
@spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
# wait a period of time and return newest version of the User structs
# this is because we have synchronous follow APIs and need to simulate them
# with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
with %User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
nil -> :error
end
end
def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout),
%User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
nil -> :error
end
end
def parse_bio(bio) when is_binary(bio) and bio != "" do
bio
|> CommonUtils.format_input("text/plain", mentions_format: :full)
|> elem(0)
end
def parse_bio(_), do: ""
def parse_bio(bio, user) when is_binary(bio) and bio != "" do
# TODO: get profile URLs other than user.ap_id
profile_urls = [user.ap_id]
bio
|> CommonUtils.format_input("text/plain",
mentions_format: :full,
rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
)
|> elem(0)
end
def parse_bio(_, _), do: ""
def tag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
end)
end
def tag(nickname, tags) when is_binary(nickname),
do: tag(get_by_nickname(nickname), tags)
def tag(%User{} = user, tags),
do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
def untag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
end)
end
def untag(nickname, tags) when is_binary(nickname),
do: untag(get_by_nickname(nickname), tags)
def untag(%User{} = user, tags),
do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
defp update_tags(%User{} = user, new_tags) do
{:ok, updated_user} =
user
|> change(%{tags: new_tags})
|> update_and_set_cache()
updated_user
end
defp normalize_tags(tags) do
[tags]
|> List.flatten()
|> Enum.map(&String.downcase/1)
end
defp local_nickname_regex do
if Pleroma.Config.get([:instance, :extended_nickname_format]) do
@extended_local_nickname_regex
else
@strict_local_nickname_regex
end
end
def local_nickname(nickname_or_mention) do
nickname_or_mention
|> full_nickname()
|> String.split("@")
|> hd()
end
def full_nickname(nickname_or_mention),
do: String.trim_leading(nickname_or_mention, "@")
def error_user(ap_id) do
%User{
name: ap_id,
ap_id: ap_id,
- info: %User.Info{},
nickname: "erroruser@example.com",
inserted_at: NaiveDateTime.utc_now()
}
end
@spec all_superusers() :: [User.t()]
def all_superusers do
User.Query.build(%{super_users: true, local: true, deactivated: false})
|> Repo.all()
end
def showing_reblogs?(%User{} = user, %User{} = target) do
- target.ap_id not in user.info.muted_reblogs
+ target.ap_id not in user.muted_reblogs
end
@doc """
The function returns a query to get users with no activity for given interval of days.
Inactive users are those who didn't read any notification, or had any activity where
the user is the activity's actor, during `inactivity_threshold` days.
Deactivated users will not appear in this list.
## Examples
iex> Pleroma.User.list_inactive_users()
%Ecto.Query{}
"""
@spec list_inactive_users_query(integer()) :: Ecto.Query.t()
def list_inactive_users_query(inactivity_threshold \\ 7) do
negative_inactivity_threshold = -inactivity_threshold
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
# Subqueries are not supported in `where` clauses, join gets too complicated.
has_read_notifications =
from(n in Pleroma.Notification,
where: n.seen == true,
group_by: n.id,
having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
select: n.user_id
)
|> Pleroma.Repo.all()
from(u in Pleroma.User,
left_join: a in Pleroma.Activity,
on: u.ap_id == a.actor,
where: not is_nil(u.nickname),
- where: fragment("not (?->'deactivated' @> 'true')", u.info),
+ where: u.deactivated != ^true,
where: u.id not in ^has_read_notifications,
group_by: u.id,
having:
max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
is_nil(max(a.inserted_at))
)
end
@doc """
Enable or disable email notifications for user
## Examples
- iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
- Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
+ iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
+ Pleroma.User{email_notifications: %{"digest" => true}}
- iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
- Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
+ iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
+ Pleroma.User{email_notifications: %{"digest" => false}}
"""
@spec switch_email_notifications(t(), String.t(), boolean()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def switch_email_notifications(user, type, status) do
- update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
+ User.update_email_notifications(user, %{type => status})
end
@doc """
Set `last_digest_emailed_at` value for the user to current time
"""
@spec touch_last_digest_emailed_at(t()) :: t()
def touch_last_digest_emailed_at(user) do
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
{:ok, updated_user} =
user
|> change(%{last_digest_emailed_at: now})
|> update_and_set_cache()
updated_user
end
@spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
def toggle_confirmation(%User{} = user) do
- need_confirmation? = !user.info.confirmation_pending
-
user
- |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
+ |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
+ |> update_and_set_cache()
end
- def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
+ def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
mascot
end
- def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
+ def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
# use instance-default
config = Pleroma.Config.get([:assets, :mascots])
default_mascot = Pleroma.Config.get([:assets, :default_mascot])
mascot = Keyword.get(config, default_mascot)
%{
"id" => "default-mascot",
"url" => mascot[:url],
"preview_url" => mascot[:url],
"pleroma" => %{
"mime_type" => mascot[:mime_type]
}
}
end
def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
def ensure_keys_present(%User{} = user) do
with {:ok, pem} <- Keys.generate_rsa_pem() do
user
|> cast(%{keys: pem}, [:keys])
|> validate_required([:keys])
|> update_and_set_cache()
end
end
def get_ap_ids_by_nicknames(nicknames) do
from(u in User,
where: u.nickname in ^nicknames,
select: u.ap_id
)
|> Repo.all()
end
defdelegate search(query, opts \\ []), to: User.Search
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
end
defp put_password_hash(changeset), do: changeset
def is_internal_user?(%User{nickname: nil}), do: true
def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
def is_internal_user?(_), do: false
# A hack because user delete activities have a fake id for whatever reason
# TODO: Get rid of this
def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
def get_delivered_users_by_object_id(object_id) do
from(u in User,
inner_join: delivery in assoc(u, :deliveries),
where: delivery.object_id == ^object_id
)
|> Repo.all()
end
def change_email(user, email) do
user
|> cast(%{email: email}, [:email])
|> validate_required([:email])
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> update_and_set_cache()
end
- @doc """
- Changes `user.info` and returns the user changeset.
+ # Internal function; public one is `deactivate/2`
+ defp set_activation_status(user, deactivated) do
+ user
+ |> cast(%{deactivated: deactivated}, [:deactivated])
+ |> update_and_set_cache()
+ end
- `fun` is called with the `user.info`.
- """
- def change_info(user, fun) do
- changeset = change(user)
- info = get_field(changeset, :info) || %User.Info{}
- put_embed(changeset, :info, fun.(info))
+ def update_banner(user, banner) do
+ user
+ |> cast(%{banner: banner}, [:banner])
+ |> update_and_set_cache()
end
- @doc """
- Updates `user.info` and sets cache.
+ def update_background(user, background) do
+ user
+ |> cast(%{background: background}, [:background])
+ |> update_and_set_cache()
+ end
+
+ def update_source_data(user, source_data) do
+ user
+ |> cast(%{source_data: source_data}, [:source_data])
+ |> update_and_set_cache()
+ end
+
+ def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
+ %{
+ admin: is_admin,
+ moderator: is_moderator
+ }
+ end
+
+ # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
+ # For example: [{"name": "Pronoun", "value": "she/her"}, …]
+ def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
+ limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
+
+ attachment
+ |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
+ |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
+ |> Enum.take(limit)
+ end
+
+ def fields(%{fields: nil}), do: []
+
+ def fields(%{fields: fields}), do: fields
+
+ def validate_fields(changeset, remote? \\ false) do
+ limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
+ limit = Pleroma.Config.get([:instance, limit_name], 0)
+
+ changeset
+ |> validate_length(:fields, max: limit)
+ |> validate_change(:fields, fn :fields, fields ->
+ if Enum.all?(fields, &valid_field?/1) do
+ []
+ else
+ [fields: "invalid"]
+ end
+ end)
+ end
+
+ defp valid_field?(%{"name" => name, "value" => value}) do
+ name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
+ value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
+
+ is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
+ String.length(value) <= value_limit
+ end
+
+ defp valid_field?(_), do: false
+
+ defp truncate_field(%{"name" => name, "value" => value}) do
+ {name, _chopped} =
+ String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
+
+ {value, _chopped} =
+ String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
+
+ %{"name" => name, "value" => value}
+ end
+
+ def admin_api_update(user, params) do
+ user
+ |> cast(params, [
+ :is_moderator,
+ :is_admin,
+ :show_role
+ ])
+ |> update_and_set_cache()
+ end
+
+ def mascot_update(user, url) do
+ user
+ |> cast(%{mascot: url}, [:mascot])
+ |> validate_required([:mascot])
+ |> update_and_set_cache()
+ end
+
+ def mastodon_settings_update(user, settings) do
+ user
+ |> cast(%{settings: settings}, [:settings])
+ |> validate_required([:settings])
+ |> update_and_set_cache()
+ end
+
+ @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
+ def confirmation_changeset(user, need_confirmation: need_confirmation?) do
+ params =
+ if need_confirmation? do
+ %{
+ confirmation_pending: true,
+ confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
+ }
+ else
+ %{
+ confirmation_pending: false,
+ confirmation_token: nil
+ }
+ end
+
+ cast(user, params, [:confirmation_pending, :confirmation_token])
+ end
+
+ def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
+ if id not in user.pinned_activities do
+ max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
+ params = %{pinned_activities: user.pinned_activities ++ [id]}
+
+ user
+ |> cast(params, [:pinned_activities])
+ |> validate_length(:pinned_activities,
+ max: max_pinned_statuses,
+ message: "You have already pinned the maximum number of statuses"
+ )
+ else
+ change(user)
+ end
+ |> update_and_set_cache()
+ end
+
+ def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
+ params = %{pinned_activities: List.delete(user.pinned_activities, id)}
+
+ user
+ |> cast(params, [:pinned_activities])
+ |> update_and_set_cache()
+ end
+
+ def update_email_notifications(user, settings) do
+ email_notifications =
+ user.email_notifications
+ |> Map.merge(settings)
+ |> Map.take(["digest"])
+
+ params = %{email_notifications: email_notifications}
+ fields = [:email_notifications]
+
+ user
+ |> cast(params, fields)
+ |> validate_required(fields)
+ |> update_and_set_cache()
+ end
+
+ defp set_subscribers(user, subscribers) do
+ params = %{subscribers: subscribers}
+
+ user
+ |> cast(params, [:subscribers])
+ |> validate_required([:subscribers])
+ |> update_and_set_cache()
+ end
+
+ def add_to_subscribers(user, subscribed) do
+ set_subscribers(user, Enum.uniq([subscribed | user.subscribers]))
+ end
+
+ def remove_from_subscribers(user, subscribed) do
+ set_subscribers(user, List.delete(user.subscribers, subscribed))
+ end
+
+ defp set_domain_blocks(user, domain_blocks) do
+ params = %{domain_blocks: domain_blocks}
+
+ user
+ |> cast(params, [:domain_blocks])
+ |> validate_required([:domain_blocks])
+ |> update_and_set_cache()
+ end
+
+ def block_domain(user, domain_blocked) do
+ set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
+ end
+
+ def unblock_domain(user, domain_blocked) do
+ set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
+ end
+
+ defp set_blocks(user, blocks) do
+ params = %{blocks: blocks}
+
+ user
+ |> cast(params, [:blocks])
+ |> validate_required([:blocks])
+ |> update_and_set_cache()
+ end
+
+ def add_to_block(user, blocked) do
+ set_blocks(user, Enum.uniq([blocked | user.blocks]))
+ end
+
+ def remove_from_block(user, blocked) do
+ set_blocks(user, List.delete(user.blocks, blocked))
+ end
+
+ defp set_mutes(user, mutes) do
+ params = %{mutes: mutes}
+
+ user
+ |> cast(params, [:mutes])
+ |> validate_required([:mutes])
+ |> update_and_set_cache()
+ end
+
+ def add_to_mutes(user, muted, notifications?) do
+ with {:ok, user} <- set_mutes(user, Enum.uniq([muted | user.mutes])) do
+ set_notification_mutes(
+ user,
+ Enum.uniq([muted | user.muted_notifications]),
+ notifications?
+ )
+ end
+ end
+
+ def remove_from_mutes(user, muted) do
+ with {:ok, user} <- set_mutes(user, List.delete(user.mutes, muted)) do
+ set_notification_mutes(
+ user,
+ List.delete(user.muted_notifications, muted),
+ true
+ )
+ end
+ end
+
+ defp set_notification_mutes(user, _muted_notifications, false = _notifications?) do
+ {:ok, user}
+ end
+
+ defp set_notification_mutes(user, muted_notifications, true = _notifications?) do
+ params = %{muted_notifications: muted_notifications}
+
+ user
+ |> cast(params, [:muted_notifications])
+ |> validate_required([:muted_notifications])
+ |> update_and_set_cache()
+ end
+
+ def add_reblog_mute(user, ap_id) do
+ params = %{muted_reblogs: user.muted_reblogs ++ [ap_id]}
+
+ user
+ |> cast(params, [:muted_reblogs])
+ |> update_and_set_cache()
+ end
+
+ def remove_reblog_mute(user, ap_id) do
+ params = %{muted_reblogs: List.delete(user.muted_reblogs, ap_id)}
+
+ user
+ |> cast(params, [:muted_reblogs])
+ |> update_and_set_cache()
+ end
+
+ def set_invisible(user, invisible) do
+ params = %{invisible: invisible}
- `fun` is called with the `user.info`.
- """
- def update_info(user, fun) do
user
- |> change_info(fun)
+ |> cast(params, [:invisible])
+ |> validate_required([:invisible])
|> update_and_set_cache()
end
end
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
deleted file mode 100644
index 4b5b43d7f..000000000
--- a/lib/pleroma/user/info.ex
+++ /dev/null
@@ -1,478 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.User.Info do
- use Ecto.Schema
- import Ecto.Changeset
-
- alias Pleroma.User.Info
-
- @type t :: %__MODULE__{}
-
- embedded_schema do
- field(:banner, :map, default: %{})
- field(:background, :map, default: %{})
- field(:source_data, :map, default: %{})
- field(:note_count, :integer, default: 0)
- field(:follower_count, :integer, default: 0)
- # Should be filled in only for remote users
- field(:following_count, :integer, default: nil)
- field(:locked, :boolean, default: false)
- field(:confirmation_pending, :boolean, default: false)
- field(:password_reset_pending, :boolean, default: false)
- field(:confirmation_token, :string, default: nil)
- field(:default_scope, :string, default: "public")
- field(:blocks, {:array, :string}, default: [])
- field(:domain_blocks, {:array, :string}, default: [])
- field(:mutes, {:array, :string}, default: [])
- field(:muted_reblogs, {:array, :string}, default: [])
- field(:muted_notifications, {:array, :string}, default: [])
- field(:subscribers, {:array, :string}, default: [])
- field(:deactivated, :boolean, default: false)
- field(:no_rich_text, :boolean, default: false)
- field(:ap_enabled, :boolean, default: false)
- field(:is_moderator, :boolean, default: false)
- field(:is_admin, :boolean, default: false)
- field(:show_role, :boolean, default: true)
- field(:keys, :string, default: nil)
- field(:settings, :map, default: nil)
- field(:magic_key, :string, default: nil)
- field(:uri, :string, default: nil)
- field(:topic, :string, default: nil)
- field(:hub, :string, default: nil)
- field(:salmon, :string, default: nil)
- field(:hide_followers_count, :boolean, default: false)
- field(:hide_follows_count, :boolean, default: false)
- field(:hide_followers, :boolean, default: false)
- field(:hide_follows, :boolean, default: false)
- field(:hide_favorites, :boolean, default: true)
- field(:unread_conversation_count, :integer, default: 0)
- field(:pinned_activities, {:array, :string}, default: [])
- field(:email_notifications, :map, default: %{"digest" => false})
- field(:mascot, :map, default: nil)
- field(:emoji, {:array, :map}, default: [])
- field(:pleroma_settings_store, :map, default: %{})
- field(:fields, {:array, :map}, default: nil)
- field(:raw_fields, {:array, :map}, default: [])
- field(:discoverable, :boolean, default: false)
-
- field(:notification_settings, :map,
- default: %{
- "followers" => true,
- "follows" => true,
- "non_follows" => true,
- "non_followers" => true
- }
- )
-
- field(:skip_thread_containment, :boolean, default: false)
-
- # Found in the wild
- # ap_id -> Where is this used?
- # bio -> Where is this used?
- # avatar -> Where is this used?
- # fqn -> Where is this used?
- # host -> Where is this used?
- # subject _> Where is this used?
- end
-
- def set_activation_status(info, deactivated) do
- params = %{deactivated: deactivated}
-
- info
- |> cast(params, [:deactivated])
- |> validate_required([:deactivated])
- end
-
- def set_password_reset_pending(info, pending) do
- params = %{password_reset_pending: pending}
-
- info
- |> cast(params, [:password_reset_pending])
- |> validate_required([:password_reset_pending])
- end
-
- def update_notification_settings(info, settings) do
- settings =
- settings
- |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
- |> Map.new()
-
- notification_settings =
- info.notification_settings
- |> Map.merge(settings)
- |> Map.take(["followers", "follows", "non_follows", "non_followers"])
-
- params = %{notification_settings: notification_settings}
-
- info
- |> cast(params, [:notification_settings])
- |> validate_required([:notification_settings])
- end
-
- @doc """
- Update email notifications in the given User.Info struct.
-
- Examples:
-
- iex> update_email_notifications(%Pleroma.User.Info{email_notifications: %{"digest" => false}}, %{"digest" => true})
- %Pleroma.User.Info{email_notifications: %{"digest" => true}}
-
- """
- @spec update_email_notifications(t(), map()) :: Ecto.Changeset.t()
- def update_email_notifications(info, settings) do
- email_notifications =
- info.email_notifications
- |> Map.merge(settings)
- |> Map.take(["digest"])
-
- params = %{email_notifications: email_notifications}
- fields = [:email_notifications]
-
- info
- |> cast(params, fields)
- |> validate_required(fields)
- end
-
- def add_to_note_count(info, number) do
- set_note_count(info, info.note_count + number)
- end
-
- def set_note_count(info, number) do
- params = %{note_count: Enum.max([0, number])}
-
- info
- |> cast(params, [:note_count])
- |> validate_required([:note_count])
- end
-
- def set_follower_count(info, number) do
- params = %{follower_count: Enum.max([0, number])}
-
- info
- |> cast(params, [:follower_count])
- |> validate_required([:follower_count])
- end
-
- def set_mutes(info, mutes) do
- params = %{mutes: mutes}
-
- info
- |> cast(params, [:mutes])
- |> validate_required([:mutes])
- end
-
- @spec set_notification_mutes(Changeset.t(), [String.t()], boolean()) :: Changeset.t()
- def set_notification_mutes(changeset, muted_notifications, notifications?) do
- if notifications? do
- put_change(changeset, :muted_notifications, muted_notifications)
- |> validate_required([:muted_notifications])
- else
- changeset
- end
- end
-
- def set_blocks(info, blocks) do
- params = %{blocks: blocks}
-
- info
- |> cast(params, [:blocks])
- |> validate_required([:blocks])
- end
-
- def set_subscribers(info, subscribers) do
- params = %{subscribers: subscribers}
-
- info
- |> cast(params, [:subscribers])
- |> validate_required([:subscribers])
- end
-
- @spec add_to_mutes(Info.t(), String.t(), boolean()) :: Changeset.t()
- def add_to_mutes(info, muted, notifications?) do
- info
- |> set_mutes(Enum.uniq([muted | info.mutes]))
- |> set_notification_mutes(
- Enum.uniq([muted | info.muted_notifications]),
- notifications?
- )
- end
-
- @spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t()
- def remove_from_mutes(info, muted) do
- info
- |> set_mutes(List.delete(info.mutes, muted))
- |> set_notification_mutes(List.delete(info.muted_notifications, muted), true)
- end
-
- def add_to_block(info, blocked) do
- set_blocks(info, Enum.uniq([blocked | info.blocks]))
- end
-
- def remove_from_block(info, blocked) do
- set_blocks(info, List.delete(info.blocks, blocked))
- end
-
- def add_to_subscribers(info, subscribed) do
- set_subscribers(info, Enum.uniq([subscribed | info.subscribers]))
- end
-
- def remove_from_subscribers(info, subscribed) do
- set_subscribers(info, List.delete(info.subscribers, subscribed))
- end
-
- def set_domain_blocks(info, domain_blocks) do
- params = %{domain_blocks: domain_blocks}
-
- info
- |> cast(params, [:domain_blocks])
- |> validate_required([:domain_blocks])
- end
-
- def add_to_domain_block(info, domain_blocked) do
- set_domain_blocks(info, Enum.uniq([domain_blocked | info.domain_blocks]))
- end
-
- def remove_from_domain_block(info, domain_blocked) do
- set_domain_blocks(info, List.delete(info.domain_blocks, domain_blocked))
- end
-
- def set_keys(info, keys) do
- params = %{keys: keys}
-
- info
- |> cast(params, [:keys])
- |> validate_required([:keys])
- end
-
- def remote_user_creation(info, params) do
- params =
- if Map.has_key?(params, :fields) do
- Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
- else
- params
- end
-
- info
- |> cast(params, [
- :ap_enabled,
- :source_data,
- :banner,
- :locked,
- :magic_key,
- :uri,
- :hub,
- :topic,
- :salmon,
- :hide_followers,
- :hide_follows,
- :hide_followers_count,
- :hide_follows_count,
- :follower_count,
- :fields,
- :following_count,
- :discoverable
- ])
- |> validate_fields(true)
- end
-
- def user_upgrade(info, params, remote? \\ false) do
- info
- |> cast(params, [
- :ap_enabled,
- :source_data,
- :banner,
- :locked,
- :magic_key,
- :follower_count,
- :following_count,
- :hide_follows,
- :fields,
- :hide_followers,
- :discoverable,
- :hide_followers_count,
- :hide_follows_count
- ])
- |> validate_fields(remote?)
- end
-
- def profile_update(info, params) do
- info
- |> cast(params, [
- :locked,
- :no_rich_text,
- :default_scope,
- :banner,
- :hide_follows,
- :hide_followers,
- :hide_followers_count,
- :hide_follows_count,
- :hide_favorites,
- :background,
- :show_role,
- :skip_thread_containment,
- :fields,
- :raw_fields,
- :pleroma_settings_store,
- :discoverable
- ])
- |> validate_fields()
- end
-
- def validate_fields(changeset, remote? \\ false) do
- limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
- limit = Pleroma.Config.get([:instance, limit_name], 0)
-
- changeset
- |> validate_length(:fields, max: limit)
- |> validate_change(:fields, fn :fields, fields ->
- if Enum.all?(fields, &valid_field?/1) do
- []
- else
- [fields: "invalid"]
- end
- end)
- end
-
- defp valid_field?(%{"name" => name, "value" => value}) do
- name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
- value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
-
- is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
- String.length(value) <= value_limit
- end
-
- defp valid_field?(_), do: false
-
- defp truncate_field(%{"name" => name, "value" => value}) do
- {name, _chopped} =
- String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
-
- {value, _chopped} =
- String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
-
- %{"name" => name, "value" => value}
- end
-
- @spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t()
- def confirmation_changeset(info, opts) do
- need_confirmation? = Keyword.get(opts, :need_confirmation)
-
- params =
- if need_confirmation? do
- %{
- confirmation_pending: true,
- confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
- }
- else
- %{
- confirmation_pending: false,
- confirmation_token: nil
- }
- end
-
- cast(info, params, [:confirmation_pending, :confirmation_token])
- end
-
- def mastodon_settings_update(info, settings) do
- params = %{settings: settings}
-
- info
- |> cast(params, [:settings])
- |> validate_required([:settings])
- end
-
- def mascot_update(info, url) do
- params = %{mascot: url}
-
- info
- |> cast(params, [:mascot])
- |> validate_required([:mascot])
- end
-
- def set_source_data(info, source_data) do
- params = %{source_data: source_data}
-
- info
- |> cast(params, [:source_data])
- |> validate_required([:source_data])
- end
-
- def admin_api_update(info, params) do
- info
- |> cast(params, [
- :is_moderator,
- :is_admin,
- :show_role
- ])
- end
-
- def add_pinnned_activity(info, %Pleroma.Activity{id: id}) do
- if id not in info.pinned_activities do
- max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
- params = %{pinned_activities: info.pinned_activities ++ [id]}
-
- info
- |> cast(params, [:pinned_activities])
- |> validate_length(:pinned_activities,
- max: max_pinned_statuses,
- message: "You have already pinned the maximum number of statuses"
- )
- else
- change(info)
- end
- end
-
- def remove_pinnned_activity(info, %Pleroma.Activity{id: id}) do
- params = %{pinned_activities: List.delete(info.pinned_activities, id)}
-
- cast(info, params, [:pinned_activities])
- end
-
- def roles(%Info{is_moderator: is_moderator, is_admin: is_admin}) do
- %{
- admin: is_admin,
- moderator: is_moderator
- }
- end
-
- def add_reblog_mute(info, ap_id) do
- params = %{muted_reblogs: info.muted_reblogs ++ [ap_id]}
-
- cast(info, params, [:muted_reblogs])
- end
-
- def remove_reblog_mute(info, ap_id) do
- params = %{muted_reblogs: List.delete(info.muted_reblogs, ap_id)}
-
- cast(info, params, [:muted_reblogs])
- end
-
- # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
- # For example: [{"name": "Pronoun", "value": "she/her"}, …]
- def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
- limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
-
- attachment
- |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
- |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
- |> Enum.take(limit)
- end
-
- def fields(%{fields: nil}), do: []
-
- def fields(%{fields: fields}), do: fields
-
- def follow_information_update(info, params) do
- info
- |> cast(params, [
- :hide_followers,
- :hide_follows,
- :follower_count,
- :following_count,
- :hide_followers_count,
- :hide_follows_count
- ])
- end
-end
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index 2baf016cf..7f5273c4e 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -1,169 +1,178 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Query do
@moduledoc """
User query builder module. Builds query from new query or another user query.
## Example:
query = Pleroma.User.Query.build(%{nickname: "nickname"})
another_query = Pleroma.User.Query.build(query, %{email: "email@example.com"})
Pleroma.Repo.all(query)
Pleroma.Repo.all(another_query)
Adding new rules:
- *ilike criteria*
- add field to @ilike_criteria list
- pass non empty string
- e.g. Pleroma.User.Query.build(%{nickname: "nickname"})
- *equal criteria*
- add field to @equal_criteria list
- pass non empty string
- e.g. Pleroma.User.Query.build(%{email: "email@example.com"})
- *contains criteria*
- add field to @containns_criteria list
- pass values list
- e.g. Pleroma.User.Query.build(%{ap_id: ["http://ap_id1", "http://ap_id2"]})
"""
import Ecto.Query
import Pleroma.Web.AdminAPI.Search, only: [not_empty_string: 1]
alias Pleroma.User
@type criteria ::
%{
query: String.t(),
tags: [String.t()],
name: String.t(),
email: String.t(),
local: boolean(),
external: boolean(),
active: boolean(),
deactivated: boolean(),
is_admin: boolean(),
is_moderator: boolean(),
super_users: boolean(),
followers: User.t(),
friends: User.t(),
recipients_from_activity: [String.t()],
nickname: [String.t()],
ap_id: [String.t()],
order_by: term(),
select: term(),
limit: pos_integer()
}
| %{}
@ilike_criteria [:nickname, :name, :query]
@equal_criteria [:email]
- @role_criteria [:is_admin, :is_moderator]
@contains_criteria [:ap_id, :nickname]
@spec build(criteria()) :: Query.t()
def build(query \\ base_query(), criteria) do
prepare_query(query, criteria)
end
@spec paginate(Ecto.Query.t(), pos_integer(), pos_integer()) :: Ecto.Query.t()
def paginate(query, page, page_size) do
from(u in query,
limit: ^page_size,
offset: ^((page - 1) * page_size)
)
end
defp base_query do
from(u in User)
end
defp prepare_query(query, criteria) do
Enum.reduce(criteria, query, &compose_query/2)
end
defp compose_query({key, value}, query)
when key in @ilike_criteria and not_empty_string(value) do
# hack for :query key
key = if key == :query, do: :nickname, else: key
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
end
defp compose_query({key, value}, query)
when key in @equal_criteria and not_empty_string(value) do
where(query, [u], ^[{key, value}])
end
defp compose_query({key, values}, query) when key in @contains_criteria and is_list(values) do
where(query, [u], field(u, ^key) in ^values)
end
defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do
Enum.reduce(tags, query, &prepare_tag_criteria/2)
end
- defp compose_query({key, _}, query) when key in @role_criteria do
- where(query, [u], fragment("(?->? @> 'true')", u.info, ^to_string(key)))
+ defp compose_query({:is_admin, _}, query) do
+ where(query, [u], u.is_admin)
+ end
+
+ defp compose_query({:is_moderator, _}, query) do
+ where(query, [u], u.is_moderator)
end
defp compose_query({:super_users, _}, query) do
where(
query,
[u],
- fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
+ u.is_admin or u.is_moderator
)
end
defp compose_query({:local, _}, query), do: location_query(query, true)
defp compose_query({:external, _}, query), do: location_query(query, false)
defp compose_query({:active, _}, query) do
- where(query, [u], fragment("not (?->'deactivated' @> 'true')", u.info))
+ User.restrict_deactivated(query)
+ |> where([u], not is_nil(u.nickname))
+ end
+
+ defp compose_query({:legacy_active, _}, query) do
+ query
+ |> where([u], fragment("not (?->'deactivated' @> 'true')", u.info))
|> where([u], not is_nil(u.nickname))
end
defp compose_query({:deactivated, false}, query) do
User.restrict_deactivated(query)
end
defp compose_query({:deactivated, true}, query) do
- where(query, [u], fragment("?->'deactivated' @> 'true'", u.info))
+ where(query, [u], u.deactivated == ^true)
|> where([u], not is_nil(u.nickname))
end
defp compose_query({:followers, %User{id: id, follower_address: follower_address}}, query) do
where(query, [u], fragment("? <@ ?", ^[follower_address], u.following))
|> where([u], u.id != ^id)
end
defp compose_query({:friends, %User{id: id, following: following}}, query) do
where(query, [u], u.follower_address in ^following)
|> where([u], u.id != ^id)
end
defp compose_query({:recipients_from_activity, to}, query) do
where(query, [u], u.ap_id in ^to or fragment("? && ?", u.following, ^to))
end
defp compose_query({:order_by, key}, query) do
order_by(query, [u], field(u, ^key))
end
defp compose_query({:select, keys}, query) do
select(query, [u], ^keys)
end
defp compose_query({:limit, limit}, query) do
limit(query, ^limit)
end
defp compose_query(_unsupported_param, query), do: query
defp prepare_tag_criteria(tag, query) do
or_where(query, [u], fragment("? = any(?)", ^tag, u.tags))
end
defp location_query(query, local) do
where(query, [u], u.local == ^local)
|> where([u], not is_nil(u.nickname))
end
end
diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex
index 6fb2c2352..bab8d92e2 100644
--- a/lib/pleroma/user/search.ex
+++ b/lib/pleroma/user/search.ex
@@ -1,223 +1,183 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Search do
alias Pleroma.Pagination
- alias Pleroma.Repo
alias Pleroma.User
import Ecto.Query
- @similarity_threshold 0.25
@limit 20
def search(query_string, opts \\ []) do
resolve = Keyword.get(opts, :resolve, false)
following = Keyword.get(opts, :following, false)
result_limit = Keyword.get(opts, :limit, @limit)
offset = Keyword.get(opts, :offset, 0)
for_user = Keyword.get(opts, :for_user)
query_string = format_query(query_string)
maybe_resolve(resolve, for_user, query_string)
- {:ok, results} =
- Repo.transaction(fn ->
- Ecto.Adapters.SQL.query(
- Repo,
- "select set_limit(#{@similarity_threshold})",
- []
- )
-
- query_string
- |> search_query(for_user, following)
- |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
- end)
+ results =
+ query_string
+ |> search_query(for_user, following)
+ |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
results
end
defp format_query(query_string) do
# Strip the beginning @ off if there is a query
query_string = String.trim_leading(query_string, "@")
with [name, domain] <- String.split(query_string, "@"),
formatted_domain <- String.replace(domain, ~r/[!-\-|@|[-`|{-~|\/|:|\s]+/, "") do
name <> "@" <> to_string(:idna.encode(formatted_domain))
else
_ -> query_string
end
end
defp search_query(query_string, for_user, following) do
for_user
|> base_query(following)
|> filter_blocked_user(for_user)
|> filter_blocked_domains(for_user)
- |> search_subqueries(query_string)
- |> union_subqueries
- |> distinct_query()
- |> boost_search_rank_query(for_user)
+ |> fts_search(query_string)
+ |> trigram_rank(query_string)
+ |> boost_search_rank(for_user)
|> subquery()
|> order_by(desc: :search_rank)
|> maybe_restrict_local(for_user)
end
+ @nickname_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~\-@]+$/
+ defp fts_search(query, query_string) do
+ {nickname_weight, name_weight} =
+ if String.match?(query_string, @nickname_regex) do
+ {"A", "B"}
+ else
+ {"B", "A"}
+ end
+
+ query_string = to_tsquery(query_string)
+
+ from(
+ u in query,
+ where:
+ fragment(
+ """
+ (setweight(to_tsvector('simple', ?), ?) || setweight(to_tsvector('simple', ?), ?)) @@ to_tsquery('simple', ?)
+ """,
+ u.name,
+ ^name_weight,
+ u.nickname,
+ ^nickname_weight,
+ ^query_string
+ )
+ )
+ end
+
+ defp to_tsquery(query_string) do
+ String.trim_trailing(query_string, "@" <> local_domain())
+ |> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
+ |> String.trim()
+ |> String.split()
+ |> Enum.map(&(&1 <> ":*"))
+ |> Enum.join(" | ")
+ end
+
+ defp trigram_rank(query, query_string) do
+ from(
+ u in query,
+ select_merge: %{
+ search_rank:
+ fragment(
+ "similarity(?, trim(? || ' ' || coalesce(?, '')))",
+ ^query_string,
+ u.nickname,
+ u.name
+ )
+ }
+ )
+ end
+
defp base_query(_user, false), do: User
defp base_query(user, true), do: User.get_followers_query(user)
- defp filter_blocked_user(query, %User{info: %{blocks: blocks}})
+ defp filter_blocked_user(query, %User{blocks: blocks})
when length(blocks) > 0 do
from(q in query, where: not (q.ap_id in ^blocks))
end
defp filter_blocked_user(query, _), do: query
- defp filter_blocked_domains(query, %User{info: %{domain_blocks: domain_blocks}})
+ defp filter_blocked_domains(query, %User{domain_blocks: domain_blocks})
when length(domain_blocks) > 0 do
domains = Enum.join(domain_blocks, ",")
from(
q in query,
where: fragment("substring(ap_id from '.*://([^/]*)') NOT IN (?)", ^domains)
)
end
defp filter_blocked_domains(query, _), do: query
- defp union_subqueries({fts_subquery, trigram_subquery}) do
- from(s in trigram_subquery, union_all: ^fts_subquery)
- end
-
- defp search_subqueries(base_query, query_string) do
- {
- fts_search_subquery(base_query, query_string),
- trigram_search_subquery(base_query, query_string)
- }
- end
-
- defp distinct_query(q) do
- from(s in subquery(q), order_by: s.search_type, distinct: s.id)
- end
-
defp maybe_resolve(true, user, query) do
case {limit(), user} do
{:all, _} -> :noop
{:unauthenticated, %User{}} -> User.get_or_fetch(query)
{:unauthenticated, _} -> :noop
{false, _} -> User.get_or_fetch(query)
end
end
defp maybe_resolve(_, _, _), do: :noop
defp maybe_restrict_local(q, user) do
case {limit(), user} do
{:all, _} -> restrict_local(q)
{:unauthenticated, %User{}} -> q
{:unauthenticated, _} -> restrict_local(q)
{false, _} -> q
end
end
defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
defp restrict_local(q), do: where(q, [u], u.local == true)
- defp boost_search_rank_query(query, nil), do: query
+ defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
- defp boost_search_rank_query(query, for_user) do
+ defp boost_search_rank(query, %User{} = for_user) do
friends_ids = User.get_friends_ids(for_user)
followers_ids = User.get_followers_ids(for_user)
from(u in subquery(query),
select_merge: %{
search_rank:
fragment(
"""
- CASE WHEN (?) THEN 0.5 + (?) * 1.3
- WHEN (?) THEN 0.5 + (?) * 1.2
+ CASE WHEN (?) THEN (?) * 1.5
+ WHEN (?) THEN (?) * 1.3
WHEN (?) THEN (?) * 1.1
ELSE (?) END
""",
u.id in ^friends_ids and u.id in ^followers_ids,
u.search_rank,
u.id in ^friends_ids,
u.search_rank,
u.id in ^followers_ids,
u.search_rank,
u.search_rank
)
}
)
end
- @spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
- defp fts_search_subquery(query, term) do
- processed_query =
- String.trim_trailing(term, "@" <> local_domain())
- |> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
- |> String.trim()
- |> String.split()
- |> Enum.map(&(&1 <> ":*"))
- |> Enum.join(" | ")
-
- from(
- u in query,
- select_merge: %{
- search_type: ^0,
- search_rank:
- fragment(
- """
- ts_rank_cd(
- setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
- setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
- to_tsquery('simple', ?),
- 32
- )
- """,
- u.nickname,
- u.name,
- ^processed_query
- )
- },
- where:
- fragment(
- """
- (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
- setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
- """,
- u.nickname,
- u.name,
- ^processed_query
- )
- )
- |> User.restrict_deactivated()
- end
-
- @spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
- defp trigram_search_subquery(query, term) do
- term = String.trim_trailing(term, "@" <> local_domain())
-
- from(
- u in query,
- select_merge: %{
- # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
- search_type: fragment("?", 1),
- search_rank:
- fragment(
- "similarity(?, trim(? || ' ' || coalesce(?, '')))",
- ^term,
- u.nickname,
- u.name
- )
- },
- where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
- )
- |> User.restrict_deactivated()
- end
-
- defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
+ defp boost_search_rank(query, _for_user), do: query
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 4cdf4876e..40f3d3781 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,1247 +1,1261 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics
alias Pleroma.Config
alias Pleroma.Conversation
+ alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.Upload
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger
alias Pleroma.Workers.BackgroundWorker
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
import Pleroma.Web.ActivityPub.Visibility
require Logger
require Pleroma.Constants
# For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
actor = User.get_cached_by_ap_id(data["actor"])
recipients =
Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
case User.get_cached_by_ap_id(recipient) do
nil -> true
user -> User.following?(user, actor)
end
end)
{recipients, to, cc}
end
defp get_recipients(%{"type" => "Create"} = data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
actor = Map.get(data, "actor", [])
recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq()
{recipients, to, cc}
end
defp get_recipients(data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
recipients = Enum.concat([to, cc, bcc])
{recipients, to, cc}
end
defp check_actor_is_active(actor) do
if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor),
- false <- user.info.deactivated do
+ false <- user.deactivated do
true
else
_e -> false
end
else
true
end
end
defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
limit = Config.get([:instance, :remote_limit])
String.length(content) <= limit
end
defp check_remote_limit(_), do: true
def increase_note_count_if_public(actor, object) do
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end
def decrease_note_count_if_public(actor, object) do
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
def increase_replies_count_if_reply(%{
"object" => %{"inReplyTo" => reply_ap_id} = object,
"type" => "Create"
}) do
if is_public?(object) do
Object.increase_replies_count(reply_ap_id)
end
end
def increase_replies_count_if_reply(_create_data), do: :noop
def decrease_replies_count_if_reply(%Object{
data: %{"inReplyTo" => reply_ap_id} = object
}) do
if is_public?(object) do
Object.decrease_replies_count(reply_ap_id)
end
end
def decrease_replies_count_if_reply(_object), do: :noop
def increase_poll_votes_if_vote(%{
"object" => %{"inReplyTo" => reply_ap_id, "name" => name},
"type" => "Create"
}) do
Object.increase_vote_count(reply_ap_id, name)
end
def increase_poll_votes_if_vote(_create_data), do: :noop
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake),
true <- bypass_actor_check || check_actor_is_active(map["actor"]),
{_, true} <- {:remote_limit_error, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
- :ok <- Containment.contain_child(map),
+ {:containment, :ok} <- {:containment, Containment.contain_child(map)},
{:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
Repo.insert(%Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients
})
# Splice in the child object if we have one.
activity =
if not is_nil(object) do
Map.put(activity, :object, object)
else
activity
end
BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
Notification.create_notifications(activity)
- participations =
- activity
- |> Conversation.create_or_bump_for()
- |> get_participations()
-
+ conversation = create_or_bump_conversation(activity, map["actor"])
+ participations = get_participations(conversation)
stream_out(activity)
stream_out_participations(participations)
{:ok, activity}
else
%Activity{} = activity ->
{:ok, activity}
{:fake, true, map, recipients} ->
activity = %Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients,
id: "pleroma:fakeid"
}
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
{:ok, activity}
error ->
{:error, error}
end
end
- defp get_participations({:ok, %{participations: participations}}), do: participations
+ defp create_or_bump_conversation(activity, actor) do
+ with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
+ %User{} = user <- User.get_cached_by_ap_id(actor),
+ Participation.mark_as_read(user, conversation) do
+ {:ok, conversation}
+ end
+ end
+
+ defp get_participations({:ok, conversation}) do
+ conversation
+ |> Repo.preload(:participations, force: true)
+ |> Map.get(:participations)
+ end
+
defp get_participations(_), do: []
def stream_out_participations(participations) do
participations =
participations
|> Repo.preload(:user)
Streamer.stream("participation", participations)
end
def stream_out_participations(%Object{data: %{"context" => context}}, user) do
with %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
conversation = Repo.preload(conversation, :participations),
last_activity_id =
fetch_latest_activity_id_for_context(conversation.ap_id, %{
"user" => user,
"blocking_user" => user
}) do
if last_activity_id do
stream_out_participations(conversation.participations)
end
end
end
def stream_out_participations(_, _), do: :noop
def stream_out(%Activity{data: %{"type" => data_type}} = activity)
when data_type in ["Create", "Announce", "Delete"] do
activity
|> Topics.get_activity_topics()
|> Streamer.stream(activity)
end
def stream_out(_activity) do
:noop
end
def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
+ quick_insert? = Pleroma.Config.get([:env]) == :benchmark
with create_data <-
make_create_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
),
{:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
_ <- increase_poll_votes_if_vote(create_data),
- # Changing note count prior to enqueuing federation task in order to avoid
- # race conditions on updating user.info
+ {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
+ {:quick_insert, true, activity} ->
+ {:ok, activity}
+
{:fake, true, activity} ->
{:ok, activity}
{:error, message} ->
{:error, message}
end
end
def listen(%{to: to, actor: actor, context: context, object: object} = params) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
with listen_data <-
make_listen_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
),
{:ok, activity} <- insert(listen_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:error, message} ->
{:error, message}
end
end
def accept(params) do
accept_or_reject("Accept", params)
end
def reject(params) do
accept_or_reject("Reject", params)
end
def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
local = Map.get(params, :local, true)
activity_id = Map.get(params, :activity_id, nil)
with data <-
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|> Utils.maybe_put("id", activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
local = !(params[:local] == false)
activity_id = params[:activity_id]
with data <- %{
"to" => to,
"cc" => cc,
"type" => "Update",
"actor" => actor,
"object" => object
},
data <- Utils.maybe_put(data, "id", activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
def like(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true
) do
with nil <- get_existing_like(ap_id, object),
like_data <- make_like_data(user, object, activity_id),
{:ok, activity} <- insert(like_data, local),
{:ok, object} <- add_like_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
%Activity{} = activity -> {:ok, activity, object}
error -> {:error, error}
end
end
def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
unlike_data <- make_unlike_data(actor, like_activity, activity_id),
{:ok, unlike_activity} <- insert(unlike_data, local),
{:ok, _activity} <- Repo.delete(like_activity),
{:ok, object} <- remove_like_from_object(like_activity, object),
:ok <- maybe_federate(unlike_activity) do
{:ok, unlike_activity, like_activity, object}
else
_e -> {:ok, object}
end
end
def announce(
%User{ap_id: _} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true,
public \\ true
) do
with true <- is_announceable?(object, user, public),
announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
error -> {:error, error}
end
end
def unannounce(
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity} <- insert(unannounce_data, local),
:ok <- maybe_federate(unannounce_activity),
{:ok, _activity} <- Repo.delete(announce_activity),
{:ok, object} <- remove_announce_from_object(announce_activity, object) do
{:ok, unannounce_activity, object}
else
_e -> {:ok, object}
end
end
def follow(follower, followed, activity_id \\ nil, local \\ true) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity),
_ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do
{:ok, activity}
end
end
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do
with data <- %{
"to" => [follower_address],
"type" => "Delete",
"actor" => ap_id,
"object" => %{"type" => "Person", "id" => ap_id}
},
{:ok, activity} <- insert(data, true, true, true),
:ok <- maybe_federate(activity) do
{:ok, user}
end
end
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options \\ []) do
local = Keyword.get(options, :local, true)
activity_id = Keyword.get(options, :activity_id, nil)
actor = Keyword.get(options, :actor, actor)
user = User.get_cached_by_ap_id(actor)
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
with {:ok, object, activity} <- Object.delete(object),
data <-
%{
"type" => "Delete",
"actor" => actor,
"object" => id,
"to" => to,
"deleted_activity_id" => activity && activity.id
}
|> maybe_put("id", activity_id),
{:ok, activity} <- insert(data, local, false),
stream_out_participations(object, user),
_ <- decrease_replies_count_if_reply(object),
- # Changing note count prior to enqueuing federation task in order to avoid
- # race conditions on updating user.info
{:ok, _actor} <- decrease_note_count_if_public(user, object),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@spec block(User.t(), User.t(), String.t() | nil, boolean) :: {:ok, Activity.t() | nil}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
if unfollow_blocked do
follow_activity = fetch_latest_follow(blocker, blocked)
if follow_activity, do: unfollow(blocker, blocked, nil, local)
end
with true <- outgoing_blocks,
block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
_e -> {:ok, nil}
end
end
def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
{:ok, activity} <- insert(unblock_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@spec flag(map()) :: {:ok, Activity.t()} | any
def flag(
%{
actor: actor,
context: _context,
account: account,
statuses: statuses,
content: content
} = params
) do
# only accept false as false value
local = !(params[:local] == false)
forward = !(params[:forward] == false)
additional = params[:additional] || %{}
additional =
if forward do
Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
else
Map.merge(additional, %{"to" => [], "cc" => []})
end
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
{:ok, stripped_activity} <- strip_report_status_data(activity),
:ok <- maybe_federate(stripped_activity) do
Enum.each(User.all_superusers(), fn superuser ->
superuser
|> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
|> Pleroma.Emails.Mailer.deliver_async()
end)
{:ok, activity}
end
end
defp fetch_activities_for_context_query(context, opts) do
public = [Pleroma.Constants.as_public()]
recipients =
if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
from(activity in Activity)
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts)
|> restrict_blocked(opts)
|> restrict_recipients(recipients, opts["user"])
|> where(
[activity],
fragment(
"?->>'type' = ? and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
)
)
|> exclude_poll_votes(opts)
|> exclude_id(opts)
|> order_by([activity], desc: activity.id)
end
@spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()]
def fetch_activities_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> Repo.all()
end
@spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
FlakeId.Ecto.CompatType.t() | nil
def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
|> limit(1)
|> select([a], a.id)
|> Repo.one()
end
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
opts = Map.drop(opts, ["user"])
[Pleroma.Constants.as_public()]
|> fetch_activities_query(opts)
|> restrict_unlisted()
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
end
@valid_visibilities ~w[direct unlisted public private]
defp restrict_visibility(query, %{visibility: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
query =
from(
a in query,
where:
fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
query
else
Logger.error("Could not restrict visibility to #{visibility}")
end
end
defp restrict_visibility(query, %{visibility: visibility})
when visibility in @valid_visibilities do
from(
a in query,
where:
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
)
end
defp restrict_visibility(_query, %{visibility: visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not restrict visibility to #{visibility}")
end
defp restrict_visibility(query, _visibility), do: query
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
from(
a in query,
where:
not fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
else
Logger.error("Could not exclude visibility to #{visibility}")
query
end
end
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
when visibility in @valid_visibilities do
from(
a in query,
where:
not fragment(
"activity_visibility(?, ?, ?) = ?",
a.actor,
a.recipients,
a.data,
^visibility
)
)
end
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not exclude visibility to #{visibility}")
query
end
defp exclude_visibility(query, _visibility), do: query
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
do: query
defp restrict_thread_visibility(
query,
- %{"user" => %User{info: %{skip_thread_containment: true}}},
+ %{"user" => %User{skip_thread_containment: true}},
_
),
do: query
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
)
end
defp restrict_thread_visibility(query, _, _), do: query
def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do
params =
params
|> Map.put("user", reading_user)
|> Map.put("actor_id", user.ap_id)
|> Map.put("whole_db", true)
recipients =
user_activities_recipients(%{
"godmode" => params["godmode"],
"reading_user" => reading_user
})
fetch_activities(recipients, params)
|> Enum.reverse()
end
def fetch_user_activities(user, reading_user, params \\ %{}) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("user", reading_user)
|> Map.put("actor_id", user.ap_id)
|> Map.put("whole_db", true)
- |> Map.put("pinned_activity_ids", user.info.pinned_activities)
+ |> Map.put("pinned_activity_ids", user.pinned_activities)
recipients =
user_activities_recipients(%{
"godmode" => params["godmode"],
"reading_user" => reading_user
})
fetch_activities(recipients, params)
|> Enum.reverse()
end
defp user_activities_recipients(%{"godmode" => true}) do
[]
end
defp user_activities_recipients(%{"reading_user" => reading_user}) do
if reading_user do
[Pleroma.Constants.as_public()] ++ [reading_user.ap_id | reading_user.following]
else
[Pleroma.Constants.as_public()]
end
end
defp restrict_since(query, %{"since_id" => ""}), do: query
defp restrict_since(query, %{"since_id" => since_id}) do
from(activity in query, where: activity.id > ^since_id)
end
defp restrict_since(query, _), do: query
defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
when is_list(tag_reject) and tag_reject != [] do
from(
[_activity, object] in query,
where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
)
end
defp restrict_tag_reject(query, _), do: query
defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag_all(query, %{"tag_all" => tag_all})
when is_list(tag_all) and tag_all != [] do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
)
end
defp restrict_tag_all(query, _), do: query
defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
)
end
defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
)
end
defp restrict_tag(query, _), do: query
defp restrict_recipients(query, [], _user), do: query
defp restrict_recipients(query, recipients, nil) do
from(activity in query, where: fragment("? && ?", ^recipients, activity.recipients))
end
defp restrict_recipients(query, recipients, user) do
from(
activity in query,
where: fragment("? && ?", ^recipients, activity.recipients),
or_where: activity.actor == ^user.ap_id
)
end
defp restrict_local(query, %{"local_only" => true}) do
from(activity in query, where: activity.local == true)
end
defp restrict_local(query, _), do: query
defp restrict_actor(query, %{"actor_id" => actor_id}) do
from(activity in query, where: activity.actor == ^actor_id)
end
defp restrict_actor(query, _), do: query
defp restrict_type(query, %{"type" => type}) when is_binary(type) do
from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
end
defp restrict_type(query, %{"type" => type}) do
from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
end
defp restrict_type(query, _), do: query
defp restrict_state(query, %{"state" => state}) do
from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state))
end
defp restrict_state(query, _), do: query
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
from(
[_activity, object] in query,
where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id)
)
end
defp restrict_favorited_by(query, _), do: query
defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
from(
[_activity, object] in query,
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
)
end
defp restrict_media(query, _), do: query
defp restrict_replies(query, %{"exclude_replies" => val}) when val == "true" or val == "1" do
from(
[_activity, object] in query,
where: fragment("?->>'inReplyTo' is null", object.data)
)
end
defp restrict_replies(query, _), do: query
defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end
defp restrict_reblogs(query, _), do: query
defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
- defp restrict_muted(query, %{"muting_user" => %User{info: info}} = opts) do
- mutes = info.mutes
+ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do
+ mutes = user.mutes
query =
from([activity] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
)
unless opts["skip_preload"] do
from([thread_mute: tm] in query, where: is_nil(tm.user_id))
else
query
end
end
defp restrict_muted(query, _), do: query
- defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
- blocks = info.blocks || []
- domain_blocks = info.domain_blocks || []
+ defp restrict_blocked(query, %{"blocking_user" => %User{} = user}) do
+ blocks = user.blocks || []
+ domain_blocks = user.domain_blocks || []
query =
if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
from(
[activity, object: o] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (? && ?)", activity.recipients, ^blocks),
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocks
),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks),
where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks)
)
end
defp restrict_blocked(query, _), do: query
defp restrict_unlisted(query) do
from(
activity in query,
where:
fragment(
"not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
activity.data,
^[Pleroma.Constants.as_public()]
)
)
end
defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
from(activity in query, where: activity.id in ^ids)
end
defp restrict_pinned(query, _), do: query
- defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
- muted_reblogs = info.muted_reblogs || []
+ defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user}) do
+ muted_reblogs = user.muted_reblogs || []
from(
activity in query,
where:
fragment(
"not ( ?->>'type' = 'Announce' and ? = ANY(?))",
activity.data,
activity.actor,
^muted_reblogs
)
)
end
defp restrict_muted_reblogs(query, _), do: query
defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
defp exclude_poll_votes(query, _) do
if has_named_binding?(query, :object) do
from([activity, object: o] in query,
where: fragment("not(?->>'type' = ?)", o.data, "Answer")
)
else
query
end
end
defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do
from(activity in query, where: activity.id != ^id)
end
defp exclude_id(query, _), do: query
defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
defp maybe_preload_objects(query, _) do
query
|> Activity.with_preloaded_object()
end
defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query
defp maybe_preload_bookmarks(query, opts) do
query
|> Activity.with_preloaded_bookmark(opts["user"])
end
defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query
defp maybe_set_thread_muted_field(query, opts) do
query
|> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"])
end
defp maybe_order(query, %{order: :desc}) do
query
|> order_by(desc: :id)
end
defp maybe_order(query, %{order: :asc}) do
query
|> order_by(asc: :id)
end
defp maybe_order(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do
config = %{
skip_thread_containment: Config.get([:instance, :skip_thread_containment])
}
Activity
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts)
|> restrict_tag_reject(opts)
|> restrict_tag_all(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_state(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(opts)
|> restrict_muted(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config)
|> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
|> exclude_visibility(opts)
end
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
list_memberships = Pleroma.List.memberships(opts["user"])
fetch_activities_query(recipients ++ list_memberships, opts)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
|> maybe_update_cc(list_memberships, opts["user"])
end
defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
when is_list(list_memberships) and length(list_memberships) > 0 do
Enum.map(activities, fn
%{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 ->
if Enum.any?(bcc, &(&1 in list_memberships)) do
update_in(activity.data["cc"], &[user_ap_id | &1])
else
activity
end
activity ->
activity
end)
end
defp maybe_update_cc(activities, _, _), do: activities
def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
from(activity in query,
where:
fragment("? && ?", activity.recipients, ^recipients) or
(fragment("? && ?", activity.recipients, ^recipients_with_public) and
^Pleroma.Constants.as_public() in activity.recipients)
)
end
def fetch_activities_bounded(
recipients,
recipients_with_public,
opts \\ %{},
pagination \\ :keyset
) do
fetch_activities_query([], opts)
|> fetch_activities_bounded_query(recipients, recipients_with_public)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
end
def upload(file, opts \\ []) do
with {:ok, data} <- Upload.store(file, opts) do
obj_data =
if opts[:actor] do
Map.put(data, "actor", opts[:actor])
else
data
end
Repo.insert(%Object{data: obj_data})
end
end
defp object_to_user_data(data) do
avatar =
data["icon"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["icon"]["url"]}]
}
banner =
data["image"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["image"]["url"]}]
}
fields =
data
|> Map.get("attachment", [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false
+ invisible = data["invisible"] || false
user_data = %{
ap_id: data["id"],
- info: %{
- ap_enabled: true,
- source_data: data,
- banner: banner,
- fields: fields,
- locked: locked,
- discoverable: discoverable
- },
+ ap_enabled: true,
+ source_data: data,
+ banner: banner,
+ fields: fields,
+ locked: locked,
+ discoverable: discoverable,
+ invisible: invisible,
avatar: avatar,
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
bio: data["summary"]
}
# nickname can be nil because of virtual actors
user_data =
if data["preferredUsername"] do
Map.put(
user_data,
:nickname,
"#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
)
else
Map.put(user_data, :nickname, nil)
end
{:ok, user_data}
end
def fetch_follow_information_for_user(user) do
with {:ok, following_data} <-
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address),
following_count when is_integer(following_count) <- following_data["totalItems"],
{:ok, hide_follows} <- collection_private(following_data),
{:ok, followers_data} <-
Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address),
followers_count when is_integer(followers_count) <- followers_data["totalItems"],
{:ok, hide_followers} <- collection_private(followers_data) do
{:ok,
%{
hide_follows: hide_follows,
follower_count: followers_count,
following_count: following_count,
hide_followers: hide_followers
}}
else
{:error, _} = e ->
e
e ->
{:error, e}
end
end
defp maybe_update_follow_information(data) do
with {:enabled, true} <-
{:enabled, Pleroma.Config.get([:instance, :external_user_synchronization])},
{:ok, info} <- fetch_follow_information_for_user(data) do
- info = Map.merge(data.info, info)
+ info = Map.merge(data[:info] || %{}, info)
Map.put(data, :info, info)
else
{:enabled, false} ->
data
e ->
Logger.error(
"Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e)
)
data
end
end
defp collection_private(data) do
if is_map(data["first"]) and
data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do
{:ok, false}
else
with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <-
Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do
{:ok, false}
else
{:error, {:ok, %{status: code}}} when code in [401, 403] ->
{:ok, true}
{:error, _} = e ->
e
e ->
{:error, e}
end
end
end
def user_data_from_user_object(data) do
with {:ok, data} <- MRF.filter(data),
{:ok, data} <- object_to_user_data(data) do
{:ok, data}
else
e -> {:error, e}
end
end
def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
{:ok, data} <- user_data_from_user_object(data),
data <- maybe_update_follow_information(data) do
{:ok, data}
else
- e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
+ e ->
+ Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
+ {:error, e}
end
end
def make_user_from_ap_id(ap_id) do
if _user = User.get_cached_by_ap_id(ap_id) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
User.insert_or_update_user(data)
else
e -> {:error, e}
end
end
end
def make_user_from_nickname(nickname) do
with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do
make_user_from_ap_id(ap_id)
else
_e -> {:error, "No AP id in WebFinger"}
end
end
# filter out broken threads
def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user)
end
# do post-processing on a specific activity
def contain_activity(%Activity{} = activity, %User{} = user) do
contain_broken_threads(activity, user)
end
def fetch_direct_messages_query do
Activity
|> restrict_type(%{"type" => "Create"})
|> restrict_visibility(%{visibility: "direct"})
|> order_by([activity], asc: activity.id)
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 080030eb5..568623318 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -1,508 +1,508 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.Delivery
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
require Logger
action_fallback(:errors)
plug(
Pleroma.Plugs.Cache,
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
when action in [:activity, :object]
)
plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
plug(:set_requester_reachable when action in [:inbox])
plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do
if Pleroma.Config.get([:instance, :allow_relay]) do
conn
else
conn
|> render_error(:not_found, "not found")
|> halt()
end
end
def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("user.json", %{user: user})
else
nil -> {:error, :not_found}
end
end
def object(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :object, uuid),
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)} do
conn
|> assign(:tracking_fun_data, object.id)
|> set_cache_ttl_for(object)
|> put_resp_content_type("application/activity+json")
|> put_view(ObjectView)
|> render("object.json", object: object)
else
{:public?, false} ->
{:error, :not_found}
end
end
def track_object_fetch(conn, nil), do: conn
def track_object_fetch(conn, object_id) do
with %{assigns: %{user: %User{id: user_id}}} <- conn do
Delivery.create(object_id, user_id)
end
conn
end
def activity(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :activity, uuid),
%Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:public?, Visibility.is_public?(activity)} do
conn
|> maybe_set_tracking_data(activity)
|> set_cache_ttl_for(activity)
|> put_resp_content_type("application/activity+json")
|> put_view(ObjectView)
|> render("object.json", object: activity)
else
{:public?, false} -> {:error, :not_found}
nil -> {:error, :not_found}
end
end
defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
object_id = Object.normalize(activity).id
assign(conn, :tracking_fun_data, object_id)
end
defp maybe_set_tracking_data(conn, _activity), do: conn
defp set_cache_ttl_for(conn, %Activity{object: object}) do
set_cache_ttl_for(conn, object)
end
defp set_cache_ttl_for(conn, entity) do
ttl =
case entity do
%Object{data: %{"type" => "Question"}} ->
Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
%Object{} ->
Pleroma.Config.get([:web_cache_ttl, :activity_pub])
_ ->
nil
end
assign(conn, :cache_ttl, ttl)
end
# GET /relay/following
def following(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("following.json", %{user: Relay.get_actor()})
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_follows, true} <-
- {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do
+ {:show_follows, (for_user && for_user == user) || !user.hide_follows} do
{page, _} = Integer.parse(page)
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("following.json", %{user: user, page: page, for: for_user})
else
{:show_follows, _} ->
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(403, "")
end
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("following.json", %{user: user, for: for_user})
end
end
# GET /relay/followers
def followers(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("followers.json", %{user: Relay.get_actor()})
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_followers, true} <-
- {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do
+ {:show_followers, (for_user && for_user == user) || !user.hide_followers} do
{page, _} = Integer.parse(page)
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("followers.json", %{user: user, page: page, for: for_user})
else
{:show_followers, _} ->
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(403, "")
end
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("followers.json", %{user: user, for: for_user})
end
end
def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
when page? in [true, "true"] do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
activities =
if params["max_id"] do
ActivityPub.fetch_user_activities(user, nil, %{
"max_id" => params["max_id"],
# This is a hack because postgres generates inefficient queries when filtering by
# 'Answer', poll votes will be hidden by the visibility filter in this case anyway
"include_poll_votes" => true,
"limit" => 10
})
else
ActivityPub.fetch_user_activities(user, nil, %{
"limit" => 10,
"include_poll_votes" => true
})
end
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: activities,
iri: "#{user.ap_id}/outbox"
})
end
end
def outbox(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
end
end
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
with %User{} = recipient <- User.get_cached_by_nickname(nickname),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
end
end
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
end
# only accept relayed Creates
def inbox(conn, %{"type" => "Create"} = params) do
Logger.info(
"Signature missing or not from author, relayed Create message, fetching object from source"
)
Fetcher.fetch_object_from_id(params["object"]["id"])
json(conn, "ok")
end
def inbox(conn, params) do
headers = Enum.into(conn.req_headers, %{})
if String.contains?(headers["signature"], params["actor"]) do
Logger.info(
"Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
)
Logger.info(inspect(conn.req_headers))
end
json(conn, dgettext("errors", "error"))
end
defp represent_service_actor(%User{} = user, conn) do
with {:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("user.json", %{user: user})
else
nil -> {:error, :not_found}
end
end
defp represent_service_actor(nil, _), do: {:error, :not_found}
def relay(conn, _params) do
Relay.get_actor()
|> represent_service_actor(conn)
end
def internal_fetch(conn, _params) do
InternalFetchActor.get_actor()
|> represent_service_actor(conn)
end
@doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("user.json", %{user: user})
end
def whoami(_conn, _params), do: {:error, :not_found}
def read_inbox(
%{assigns: %{user: %{nickname: nickname} = user}} = conn,
%{"nickname" => nickname, "page" => page?} = params
)
when page? in [true, "true"] do
activities =
if params["max_id"] do
ActivityPub.fetch_activities([user.ap_id | user.following], %{
"max_id" => params["max_id"],
"limit" => 10
})
else
ActivityPub.fetch_activities([user.ap_id | user.following], %{"limit" => 10})
end
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: activities,
iri: "#{user.ap_id}/inbox"
})
end
def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
"nickname" => nickname
}) do
with {:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
end
end
def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
conn
|> put_status(:forbidden)
|> json(err)
end
def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
"nickname" => nickname
}) do
err =
dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: as_nickname
)
conn
|> put_status(:forbidden)
|> json(err)
end
def handle_user_activity(user, %{"type" => "Create"} = params) do
object =
params["object"]
|> Map.merge(Map.take(params, ["to", "cc"]))
|> Map.put("attributedTo", user.ap_id())
|> Transmogrifier.fix_object()
ActivityPub.create(%{
to: params["to"],
actor: user,
context: object["context"],
object: object,
additional: Map.take(params, ["cc"])
})
end
def handle_user_activity(user, %{"type" => "Delete"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
- true <- user.info.is_moderator || user.ap_id == object.data["actor"],
+ true <- user.is_moderator || user.ap_id == object.data["actor"],
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
else
_ -> {:error, dgettext("errors", "Can't delete object")}
end
end
def handle_user_activity(user, %{"type" => "Like"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
{:ok, activity, _object} <- ActivityPub.like(user, object) do
{:ok, activity}
else
_ -> {:error, dgettext("errors", "Can't like object")}
end
end
def handle_user_activity(_, _) do
{:error, dgettext("errors", "Unhandled activity type")}
end
def update_outbox(
%{assigns: %{user: %User{nickname: nickname} = user}} = conn,
%{"nickname" => nickname} = params
) do
actor = user.ap_id()
params =
params
|> Map.drop(["id"])
|> Map.put("actor", actor)
|> Transmogrifier.fix_addressing()
with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
conn
|> put_status(:created)
|> put_resp_header("location", activity.data["id"])
|> json(activity.data)
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(message)
end
end
def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
err =
dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: user.nickname
)
conn
|> put_status(:forbidden)
|> json(err)
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
def errors(conn, _e) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "error"))
end
defp set_requester_reachable(%Plug.Conn{} = conn, _) do
with actor <- conn.params["actor"],
true <- is_binary(actor) do
Pleroma.Instances.set_reachable(actor)
end
conn
end
defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
{:ok, new_user} = User.ensure_keys_present(user)
for_user =
if new_user != user and match?(%User{}, for_user) do
User.get_cached_by_nickname(for_user.nickname)
else
for_user
end
{new_user, for_user}
end
# TODO: Add support for "object" field
@doc """
Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
Parameters:
- (required) `file`: data of the media
- (optionnal) `description`: description of the media, intended for accessibility
Response:
- HTTP Code: 201 Created
- HTTP Body: ActivityPub object to be inserted into another's `attachment` field
"""
def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
description: Map.get(data, "description")
) do
Logger.debug(inspect(object))
conn
|> put_status(:created)
|> json(object.data)
end
end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
index b90193ca0..8abe18e29 100644
--- a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
@@ -1,54 +1,54 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF
require Logger
# has the user successfully posted before?
defp old_user?(%User{} = u) do
- u.info.note_count > 0 || u.info.follower_count > 0
+ u.note_count > 0 || u.follower_count > 0
end
# does the post contain links?
defp contains_links?(%{"content" => content} = _object) do
content
|> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"],a.zrl")
|> Floki.attribute("a", "href")
|> length() > 0
end
defp contains_links?(_), do: false
@impl true
def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do
with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor),
{:contains_links, true} <- {:contains_links, contains_links?(object)},
{:old_user, true} <- {:old_user, old_user?(u)} do
{:ok, message}
else
{:contains_links, false} ->
{:ok, message}
{:old_user, false} ->
{:reject, nil}
{:error, _} ->
{:reject, nil}
e ->
Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}")
{:reject, nil}
end
end
# in all other cases, pass through
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index 3866dacee..4ea37fc7b 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -1,267 +1,267 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Delivery
alias Pleroma.HTTP
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
require Pleroma.Constants
import Pleroma.Web.ActivityPub.Visibility
@behaviour Pleroma.Web.Federator.Publisher
require Logger
@moduledoc """
ActivityPub outgoing federation module.
"""
@doc """
Determine if an activity can be represented by running it through Transmogrifier.
"""
def is_representable?(%Activity{} = activity) do
with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
true
else
_e ->
false
end
end
@doc """
Publish a single message to a peer. Takes a struct with the following
parameters set:
* `inbox`: the inbox to publish to
* `json`: the JSON message body representing the ActivityPub message
* `actor`: the actor which is signing the message
* `id`: the ActivityStreams URI of the message
"""
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
Logger.info("Federating #{id} to #{inbox}")
%{host: host, path: path} = URI.parse(inbox)
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date = Pleroma.Signature.signed_date()
signature =
Pleroma.Signature.sign(actor, %{
"(request-target)": "post #{path}",
host: host,
"content-length": byte_size(json),
digest: digest,
date: date
})
with {:ok, %{status: code}} when code in 200..299 <-
result =
HTTP.post(
inbox,
json,
[
{"Content-Type", "application/activity+json"},
{"Date", date},
{"signature", signature},
{"digest", digest}
]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(inbox)
result
else
{_post_result, response} ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
{:error, response}
end
end
def publish_one(%{actor_id: actor_id} = params) do
actor = User.get_cached_by_id(actor_id)
params
|> Map.delete(:actor_id)
|> Map.put(:actor, actor)
|> publish_one()
end
defp should_federate?(inbox, public) do
if public do
true
else
%{host: host} = URI.parse(inbox)
quarantined_instances =
Config.get([:instance, :quarantined_instances], [])
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
!Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
end
end
@spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
defp recipients(actor, activity) do
followers =
if actor.follower_address in activity.recipients do
User.get_external_followers(actor)
else
[]
end
fetchers =
with %Activity{data: %{"type" => "Delete"}} <- activity,
%Object{id: object_id} <- Object.normalize(activity),
fetchers <- User.get_delivered_users_by_object_id(object_id),
_ <- Delivery.delete_all_by_object_id(object_id) do
fetchers
else
_ ->
[]
end
- Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers ++ fetchers
+ Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
end
defp get_cc_ap_ids(ap_id, recipients) do
host = Map.get(URI.parse(ap_id), :host)
recipients
|> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
|> Enum.map(& &1.ap_id)
end
- defp maybe_use_sharedinbox(%User{info: %{source_data: data}}),
+ defp maybe_use_sharedinbox(%User{source_data: data}),
do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
@doc """
Determine a user inbox to use based on heuristics. These heuristics
are based on an approximation of the ``sharedInbox`` rules in the
[ActivityPub specification][ap-sharedinbox].
Please do not edit this function (or its children) without reading
the spec, as editing the code is likely to introduce some breakage
without some familiarity.
[ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
"""
def determine_inbox(
%Activity{data: activity_data},
- %User{info: %{source_data: data}} = user
+ %User{source_data: data} = user
) do
to = activity_data["to"] || []
cc = activity_data["cc"] || []
type = activity_data["type"]
cond do
type == "Delete" ->
maybe_use_sharedinbox(user)
Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
maybe_use_sharedinbox(user)
length(to) + length(cc) > 1 ->
maybe_use_sharedinbox(user)
true ->
data["inbox"]
end
end
@doc """
Publishes an activity with BCC to all relevant peers.
"""
def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
when is_list(bcc) and bcc != [] do
public = is_public?(activity)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
recipients = recipients(actor, activity)
recipients
|> Enum.filter(&User.ap_enabled?/1)
- |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end)
+ |> Enum.map(fn %{source_data: data} -> data["inbox"] end)
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
%User{ap_id: ap_id} =
- Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end)
+ Enum.find(recipients, fn %{source_data: data} -> data["inbox"] == inbox end)
# Get all the recipients on the same host and add them to cc. Otherwise, a remote
# instance would only accept a first message for the first recipient and ignore the rest.
cc = get_cc_ap_ids(ap_id, recipients)
json =
data
|> Map.put("cc", cc)
|> Jason.encode!()
Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
inbox: inbox,
json: json,
actor_id: actor.id,
id: activity.data["id"],
unreachable_since: unreachable_since
})
end)
end
@doc """
Publishes an activity to all relevant peers.
"""
def publish(%User{} = actor, %Activity{} = activity) do
public = is_public?(activity)
if public && Config.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
end
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
recipients(actor, activity)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %User{} = user ->
determine_inbox(activity, user)
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
Pleroma.Web.Federator.Publisher.enqueue_one(
__MODULE__,
%{
inbox: inbox,
json: json,
actor_id: actor.id,
id: activity.data["id"],
unreachable_since: unreachable_since
}
)
end)
end
def gather_webfinger_links(%User{} = user) do
[
%{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
%{
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href" => user.ap_id
}
]
end
def gather_nodeinfo_protocol_names, do: ["activitypub"]
end
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
index c2ac38907..a9434d75c 100644
--- a/lib/pleroma/web/activity_pub/relay.ex
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -1,60 +1,78 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Relay do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
require Logger
def get_actor do
- "#{Pleroma.Web.Endpoint.url()}/relay"
- |> User.get_or_create_service_actor_by_ap_id()
+ actor =
+ "#{Pleroma.Web.Endpoint.url()}/relay"
+ |> User.get_or_create_service_actor_by_ap_id()
+
+ {:ok, actor} = User.set_invisible(actor, true)
+ actor
end
@spec follow(String.t()) :: {:ok, Activity.t()} | {:error, any()}
def follow(target_instance) do
with %User{} = local_user <- get_actor(),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.follow(local_user, target_user) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
else
error -> format_error(error)
end
end
@spec unfollow(String.t()) :: {:ok, Activity.t()} | {:error, any()}
def unfollow(target_instance) do
with %User{} = local_user <- get_actor(),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
User.unfollow(local_user, target_user)
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}
else
error -> format_error(error)
end
end
@spec publish(any()) :: {:ok, Activity.t(), Object.t()} | {:error, any()}
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(),
%Object{} = object <- Object.normalize(activity) do
ActivityPub.announce(user, object, nil, true, false)
else
error -> format_error(error)
end
end
def publish(_), do: {:error, "Not implemented"}
+ @spec list() :: {:ok, [String.t()]} | {:error, any()}
+ def list do
+ with %User{following: following} = _user <- get_actor() do
+ list =
+ following
+ |> Enum.map(fn entry -> URI.parse(entry).host end)
+ |> Enum.uniq()
+
+ {:ok, list}
+ else
+ error -> format_error(error)
+ end
+ end
+
defp format_error({:error, error}), do: format_error(error)
defp format_error(error) do
Logger.error("error: #{inspect(error)}")
{:error, error}
end
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index b56343beb..9b3ee842b 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1,1140 +1,1132 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
alias Pleroma.Workers.TransmogrifierWorker
import Ecto.Query
require Logger
require Pleroma.Constants
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
def fix_object(object, options \\ []) do
object
|> strip_internal_fields
|> fix_actor
|> fix_url
|> fix_attachments
|> fix_context
|> fix_in_reply_to(options)
|> fix_emoji
|> fix_tag
|> fix_content_map
|> fix_addressing
|> fix_summary
|> fix_type(options)
end
def fix_summary(%{"summary" => nil} = object) do
Map.put(object, "summary", "")
end
def fix_summary(%{"summary" => _} = object) do
# summary is present, nothing to do
object
end
def fix_summary(object), do: Map.put(object, "summary", "")
def fix_addressing_list(map, field) do
cond do
is_binary(map[field]) ->
Map.put(map, field, [map[field]])
is_nil(map[field]) ->
Map.put(map, field, [])
true ->
map
end
end
def fix_explicit_addressing(
%{"to" => to, "cc" => cc} = object,
explicit_mentions,
follower_collection
) do
explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
final_cc =
(cc ++ explicit_cc)
|> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
|> Enum.uniq()
object
|> Map.put("to", explicit_to)
|> Map.put("cc", final_cc)
end
def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
# if directMessage flag is set to true, leave the addressing alone
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
def fix_explicit_addressing(object) do
explicit_mentions = Utils.determine_explicit_mentions(object)
%User{follower_address: follower_collection} =
object
|> Containment.get_actor()
|> User.get_cached_by_ap_id()
explicit_mentions =
explicit_mentions ++
[
Pleroma.Constants.as_public(),
follower_collection
]
fix_explicit_addressing(object, explicit_mentions, follower_collection)
end
# if as:Public is addressed, then make sure the followers collection is also addressed
# so that the activities will be delivered to local users.
def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
recipients = to ++ cc
if followers_collection not in recipients do
cond do
Pleroma.Constants.as_public() in cc ->
to = to ++ [followers_collection]
Map.put(object, "to", to)
Pleroma.Constants.as_public() in to ->
cc = cc ++ [followers_collection]
Map.put(object, "cc", cc)
true ->
object
end
else
object
end
end
def fix_implicit_addressing(object, _), do: object
def fix_addressing(object) do
{:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
followers_collection = User.ap_followers(user)
object
|> fix_addressing_list("to")
|> fix_addressing_list("cc")
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
|> fix_explicit_addressing()
|> fix_implicit_addressing(followers_collection)
end
def fix_actor(%{"attributedTo" => actor} = object) do
Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
end
def fix_in_reply_to(object, options \\ [])
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
in_reply_to_id = prepare_in_reply_to(in_reply_to)
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
if Federator.allowed_incoming_reply_depth?(options[:depth]) do
with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
%Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
e ->
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
object
end
else
object
end
end
def fix_in_reply_to(object, _options), do: object
defp prepare_in_reply_to(in_reply_to) do
cond do
is_bitstring(in_reply_to) ->
in_reply_to
is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]
is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)
true ->
""
end
end
def fix_context(object) do
context = object["context"] || object["conversation"] || Utils.generate_context_id()
object
|> Map.put("context", context)
|> Map.put("conversation", context)
end
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
Enum.map(attachment, fn data ->
media_type = data["mediaType"] || data["mimeType"]
href = data["url"] || data["href"]
url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
data
|> Map.put("mediaType", media_type)
|> Map.put("url", url)
end)
Map.put(object, "attachment", attachments)
end
def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
object
|> Map.put("attachment", [attachment])
|> fix_attachments()
end
def fix_attachments(object), do: object
def fix_url(%{"url" => url} = object) when is_map(url) do
Map.put(object, "url", url["href"])
end
def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
first_element = Enum.at(url, 0)
link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
object
|> Map.put("attachment", [first_element])
|> Map.put("url", link_element["href"])
end
def fix_url(%{"type" => object_type, "url" => url} = object)
when object_type != "Video" and is_list(url) do
first_element = Enum.at(url, 0)
url_string =
cond do
is_bitstring(first_element) -> first_element
is_map(first_element) -> first_element["href"] || ""
true -> ""
end
Map.put(object, "url", url_string)
end
def fix_url(object), do: object
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji =
tags
|> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
|> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":")
Map.put(mapping, name, data["icon"]["url"])
end)
# we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
emoji = Map.merge(object["emoji"] || %{}, emoji)
Map.put(object, "emoji", emoji)
end
def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
name = String.trim(tag["name"], ":")
emoji = %{name => tag["icon"]["url"]}
Map.put(object, "emoji", emoji)
end
def fix_emoji(object), do: object
def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
tags =
tag
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
Map.put(object, "tag", tag ++ tags)
end
def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
combined = [tag, String.slice(hashtag, 1..-1)]
Map.put(object, "tag", combined)
end
def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
def fix_tag(object), do: object
# content map usually only has one language so this will do for now.
def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map)
{_, content} = Enum.at(content_groups, 0)
Map.put(object, "content", content)
end
def fix_content_map(object), do: object
def fix_type(object, options \\ [])
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
{:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
Map.put(object, "type", "Answer")
else
_ -> object
end
end
def fix_type(object, _), do: object
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
with true <- id =~ "follows",
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
%Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
{:ok, activity}
else
_ -> {:error, nil}
end
end
defp mastodon_follow_hack(_, _), do: {:error, nil}
defp get_follow_activity(follow_object, followed) do
with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
{_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
{:ok, activity}
else
# Can't find the activity. This might a Mastodon 2.3 "Accept"
{:activity, nil} ->
mastodon_follow_hack(follow_object, followed)
_ ->
{:error, nil}
end
end
# Reduce the object list to find the reported user.
defp get_reported(objects) do
Enum.reduce_while(objects, nil, fn ap_id, _ ->
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
{:halt, user}
else
_ -> {:cont, nil}
end
end)
end
def handle_incoming(data, options \\ [])
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
# with nil ID.
def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor),
# Reduce the object list to find the reported user.
%User{} = account <- get_reported(objects),
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
%{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content,
additional: %{"cc" => [account.ap_id]}
}
|> ActivityPub.flag()
end
end
# disallow objects with bogus IDs
def handle_incoming(%{"id" => nil}, _options), do: :error
def handle_incoming(%{"id" => ""}, _options), do: :error
# length of https:// = 8, should validate better, but good enough for now.
def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
do: :error
# TODO: validate those with a Ecto scheme
# - tags
# - emoji
def handle_incoming(
%{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
options
)
when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
actor = Containment.get_actor(data)
data =
Map.put(data, "actor", actor)
|> fix_addressing
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
object = fix_object(data["object"], options)
params = %{
to: data["to"],
object: object,
actor: user,
context: object["conversation"],
local: false,
published: data["published"],
additional:
Map.take(data, [
"cc",
"directMessage",
"id"
])
}
ActivityPub.create(params)
else
%Activity{} = activity -> {:ok, activity}
_e -> :error
end
end
def handle_incoming(
%{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
options
) do
actor = Containment.get_actor(data)
data =
Map.put(data, "actor", actor)
|> fix_addressing
with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
object = fix_object(object, options)
params = %{
to: data["to"],
object: object,
actor: user,
context: nil,
local: false,
published: data["published"],
additional: Map.take(data, ["cc", "id"])
}
ActivityPub.listen(params)
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
_options
) do
with %User{local: true} = followed <-
User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
{:ok, %User{} = follower} <-
User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
{_, false} <- {:user_locked, User.locked?(followed)},
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
{_, {:ok, _}} <-
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
else
{:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
{:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
{:user_locked, true} ->
:noop
end
{:ok, activity}
else
_e ->
:error
end
end
def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _follower} = User.follow(follower, followed) do
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed,
object: follow_activity.data["id"],
local: false,
activity_id: id
})
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.reject(%{
to: follow_activity.data["to"],
type: "Reject",
actor: followed,
object: follow_activity.data["id"],
local: false,
activity_id: id
}) do
User.unfollow(follower, followed)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_embedded_obj_helper(object_id, actor),
public <- Visibility.is_public?(data),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
data,
_options
)
- when object_type in ["Person", "Application", "Service", "Organization"] do
+ when object_type in [
+ "Person",
+ "Application",
+ "Service",
+ "Organization"
+ ] do
with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
- banner = new_user_data[:info][:banner]
- locked = new_user_data[:info][:locked] || false
- attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || []
+ locked = new_user_data[:locked] || false
+ attachment = get_in(new_user_data, [:source_data, "attachment"]) || []
+ invisible = new_user_data[:invisible] || false
fields =
attachment
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
update_data =
new_user_data
- |> Map.take([:name, :bio, :avatar])
- |> Map.put(:info, %{banner: banner, locked: locked, fields: fields})
+ |> Map.take([:avatar, :banner, :bio, :name])
+ |> Map.put(:fields, fields)
+ |> Map.put(:locked, locked)
+ |> Map.put(:invisible, invisible)
actor
|> User.upgrade_changeset(update_data, true)
|> User.update_and_set_cache()
ActivityPub.update(%{
local: false,
to: data["to"] || [],
cc: data["cc"] || [],
object: object,
actor: actor_id,
activity_id: data["id"]
})
else
e ->
Logger.error(e)
:error
end
end
# TODO: We presently assume that any actor on the same origin domain as the object being
# deleted has the rights to delete that object. A better way to validate whether or not
# the object should be deleted is to refetch the object URI, which should return either
# an error or a tombstone. This would allow us to verify that a deletion actually took
# place.
def handle_incoming(
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
_options
) do
object_id = Utils.get_ap_id(object_id)
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
:ok <- Containment.contain_origin(actor.ap_id, object.data),
{:ok, activity} <-
ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
{:ok, activity}
else
nil ->
case User.get_cached_by_ap_id(object_id) do
%User{ap_id: ^actor} = user ->
User.delete(user)
nil ->
:error
end
_e ->
:error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Announce", "object" => object_id},
"actor" => _actor,
"id" => id
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Follow", "object" => followed},
"actor" => follower,
"id" => id
} = _data,
_options
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
User.unfollow(follower, followed)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Block", "object" => blocked},
"actor" => blocker,
"id" => id
} = _data,
_options
) do
with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
User.unblock(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
_options
) do
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Like", "object" => object_id},
"actor" => _actor,
"id" => id
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
# For Undos that don't have the complete object attached, try to find it in our database.
def handle_incoming(
%{
"type" => "Undo",
"object" => object
} = activity,
options
)
when is_binary(object) do
with %Activity{data: data} <- Activity.get_by_ap_id(object) do
activity
|> Map.put("object", data)
|> handle_incoming(options)
else
_e -> :error
end
end
def handle_incoming(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do
case Object.normalize(id, true, options) do
%Object{} = object -> {:ok, object}
_ -> nil
end
end
@spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
ap_id: ap_id
})
when attributed_to == ap_id do
with {:ok, activity} <-
handle_incoming(%{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => attributed_to,
"object" => data
}) do
{:ok, Object.normalize(activity)}
else
_ -> get_obj_helper(object_id)
end
end
def get_embedded_obj_helper(object_id, _) do
get_obj_helper(object_id)
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
with false <- String.starts_with?(in_reply_to, "http"),
{:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
else
_e -> object
end
end
def set_reply_to_uri(obj), do: obj
# Prepares the object of an outgoing create activity.
def prepare_object(object) do
object
|> set_sensitive
|> add_hashtags
|> add_mention_tags
|> add_emoji_tags
|> add_attributed_to
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
|> strip_internal_fields
|> strip_internal_tags
|> set_type
end
# @doc
# """
# internal -> Mastodon
# """
def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
when activity_type in ["Create", "Listen"] do
object =
object_id
|> Object.normalize()
|> Map.get(:data)
|> prepare_object
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
|> Map.delete("bcc")
{:ok, data}
end
def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
object =
object_id
|> Object.normalize()
data =
if Visibility.is_private?(object) && object.data["actor"] == ap_id do
data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
else
data |> maybe_fix_object_url
end
data =
data
|> strip_internal_fields
|> Map.merge(Utils.make_json_ld_header())
|> Map.delete("bcc")
{:ok, data}
end
# Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
# because of course it does.
def prepare_outgoing(%{"type" => "Accept"} = data) do
with follow_activity <- Activity.normalize(data["object"]) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],
"id" => follow_activity.data["id"],
"type" => "Follow"
}
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
end
def prepare_outgoing(%{"type" => "Reject"} = data) do
with follow_activity <- Activity.normalize(data["object"]) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],
"id" => follow_activity.data["id"],
"type" => "Follow"
}
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
end
def prepare_outgoing(%{"type" => _type} = data) do
data =
data
|> strip_internal_fields
|> maybe_fix_object_url
|> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
with false <- String.starts_with?(object, "http"),
{:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
%{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
relative_object do
Map.put(data, "object", external_url)
else
{:fetch, e} ->
Logger.error("Couldn't fetch #{object} #{inspect(e)}")
data
_ ->
data
end
end
def maybe_fix_object_url(data), do: data
def add_hashtags(object) do
tags =
(object["tag"] || [])
|> Enum.map(fn
# Expand internal representation tags into AS2 tags.
tag when is_binary(tag) ->
%{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
"name" => "##{tag}",
"type" => "Hashtag"
}
# Do not process tags which are already AS2 tag objects.
tag when is_map(tag) ->
tag
end)
Map.put(object, "tag", tags)
end
def add_mention_tags(object) do
mentions =
object
|> Utils.get_notified_from_object()
|> Enum.map(&build_mention_tag/1)
tags = object["tag"] || []
Map.put(object, "tag", tags ++ mentions)
end
defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
%{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
end
- def take_emoji_tags(%User{info: %{emoji: emoji} = _user_info} = _user) do
+ def take_emoji_tags(%User{emoji: emoji}) do
emoji
|> Enum.flat_map(&Map.to_list/1)
|> Enum.map(&build_emoji_tag/1)
end
# TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || []
out = Enum.map(emoji, &build_emoji_tag/1)
Map.put(object, "tag", tags ++ out)
end
def add_emoji_tags(object), do: object
defp build_emoji_tag({name, url}) do
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
"id" => url
}
end
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end
def set_sensitive(object) do
tags = object["tag"] || []
Map.put(object, "sensitive", "nsfw" in tags)
end
def set_type(%{"type" => "Answer"} = object) do
Map.put(object, "type", "Note")
end
def set_type(object), do: object
def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"]
Map.put(object, "attributedTo", attributed_to)
end
def prepare_attachments(object) do
attachments =
(object["attachment"] || [])
|> Enum.map(fn data ->
[%{"mediaType" => media_type, "href" => href} | _] = data["url"]
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
end)
Map.put(object, "attachment", attachments)
end
defp strip_internal_fields(object) do
object
|> Map.drop(Pleroma.Constants.object_internal_fields())
end
defp strip_internal_tags(%{"tag" => tags} = object) do
tags = Enum.filter(tags, fn x -> is_map(x) end)
Map.put(object, "tag", tags)
end
defp strip_internal_tags(object), do: object
def perform(:user_upgrade, user) do
# we pass a fake user so that the followers collection is stripped away
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
q =
from(
u in User,
where: ^old_follower_address in u.following,
update: [
set: [
following:
fragment(
"array_replace(?,?,?)",
u.following,
^old_follower_address,
^user.follower_address
)
]
]
)
Repo.update_all(q, [])
- maybe_retire_websub(user.ap_id)
-
q =
from(
a in Activity,
where: ^old_follower_address in a.recipients,
update: [
set: [
recipients:
fragment(
"array_replace(?,?,?)",
a.recipients,
^old_follower_address,
^user.follower_address
)
]
]
)
Repo.update_all(q, [])
end
def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
already_ap <- User.ap_enabled?(user),
{:ok, user} <- upgrade_user(user, data) do
if not already_ap do
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
end
{:ok, user}
else
%User{} = user -> {:ok, user}
e -> e
end
end
defp upgrade_user(user, data) do
user
|> User.upgrade_changeset(data, true)
|> User.update_and_set_cache()
end
- def maybe_retire_websub(ap_id) do
- # some sanity checks
- if is_binary(ap_id) && String.length(ap_id) > 8 do
- q =
- from(
- ws in Pleroma.Web.Websub.WebsubClientSubscription,
- where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
- )
-
- Repo.delete_all(q)
- end
- end
-
def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
Map.put(data, "url", url["href"])
end
def maybe_fix_user_url(data), do: data
def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 520cc1b0c..d812fd734 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -1,782 +1,788 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.Changeset
alias Ecto.UUID
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers
import Ecto.Query
require Logger
require Pleroma.Constants
@supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer", "Audio"]
@strip_status_report_states ~w(closed resolved)
@supported_report_states ~w(open closed resolved)
@valid_visibilities ~w(public unlisted private direct)
# Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have.
def get_ap_id(%{"id" => id} = _), do: id
def get_ap_id(id), do: id
def normalize_params(params) do
Map.put(params, "actor", get_ap_id(params["actor"]))
end
@spec determine_explicit_mentions(map()) :: map()
def determine_explicit_mentions(%{"tag" => tag} = _) when is_list(tag) do
Enum.flat_map(tag, fn
%{"type" => "Mention", "href" => href} -> [href]
_ -> []
end)
end
def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
object
|> Map.put("tag", [tag])
|> determine_explicit_mentions()
end
def determine_explicit_mentions(_), do: []
- @spec recipient_in_collection(any(), any()) :: boolean()
- defp recipient_in_collection(ap_id, coll) when is_binary(coll), do: ap_id == coll
- defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll
- defp recipient_in_collection(_, _), do: false
+ @spec label_in_collection?(any(), any()) :: boolean()
+ defp label_in_collection?(ap_id, coll) when is_binary(coll), do: ap_id == coll
+ defp label_in_collection?(ap_id, coll) when is_list(coll), do: ap_id in coll
+ defp label_in_collection?(_, _), do: false
+
+ @spec label_in_message?(String.t(), map()) :: boolean()
+ def label_in_message?(label, params),
+ do:
+ [params["to"], params["cc"], params["bto"], params["bcc"]]
+ |> Enum.any?(&label_in_collection?(label, &1))
+
+ @spec unaddressed_message?(map()) :: boolean()
+ def unaddressed_message?(params),
+ do:
+ [params["to"], params["cc"], params["bto"], params["bcc"]]
+ |> Enum.all?(&is_nil(&1))
@spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
- def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params) do
- addresses = [params["to"], params["cc"], params["bto"], params["bcc"]]
-
- cond do
- Enum.any?(addresses, &recipient_in_collection(ap_id, &1)) -> true
- # if the message is unaddressed at all, then assume it is directly addressed
- # to the recipient
- Enum.all?(addresses, &is_nil(&1)) -> true
- # if the message is sent from somebody the user is following, then assume it
- # is addressed to the recipient
- User.following?(recipient, actor) -> true
- true -> false
- end
- end
+ def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
+ do:
+ label_in_message?(ap_id, params) || unaddressed_message?(params) ||
+ User.following?(recipient, actor)
defp extract_list(target) when is_binary(target), do: [target]
defp extract_list(lst) when is_list(lst), do: lst
defp extract_list(_), do: []
def maybe_splice_recipient(ap_id, params) do
need_splice? =
- !recipient_in_collection(ap_id, params["to"]) &&
- !recipient_in_collection(ap_id, params["cc"])
+ !label_in_collection?(ap_id, params["to"]) &&
+ !label_in_collection?(ap_id, params["cc"])
if need_splice? do
cc_list = extract_list(params["cc"])
Map.put(params, "cc", [ap_id | cc_list])
else
params
end
end
def make_json_ld_header do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"#{Web.base_url()}/schemas/litepub-0.1.jsonld",
%{
"@language" => "und"
}
]
}
end
def make_date do
DateTime.utc_now() |> DateTime.to_iso8601()
end
def generate_activity_id do
generate_id("activities")
end
def generate_context_id do
generate_id("contexts")
end
def generate_object_id do
Helpers.o_status_url(Endpoint, :object, UUID.generate())
end
def generate_id(type) do
"#{Web.base_url()}/#{type}/#{UUID.generate()}"
end
def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
fake_create_activity = %{
"to" => object["to"],
"cc" => object["cc"],
"type" => "Create",
"object" => object
}
get_notified_from_object(fake_create_activity)
end
def get_notified_from_object(object) do
Notification.get_notified_from_activity(%Activity{data: object}, false)
end
def create_context(context) do
context = context || generate_id("contexts")
# Ecto has problems accessing the constraint inside the jsonb,
# so we explicitly check for the existed object before insert
object = Object.get_cached_by_ap_id(context)
with true <- is_nil(object),
changeset <- Object.context_mapping(context),
{:ok, inserted_object} <- Repo.insert(changeset) do
inserted_object
else
_ ->
object
end
end
@doc """
Enqueues an activity for federation if it's local
"""
@spec maybe_federate(any()) :: :ok
def maybe_federate(%Activity{local: true} = activity) do
if Pleroma.Config.get!([:instance, :federating]) do
Pleroma.Web.Federator.publish(activity)
end
:ok
end
def maybe_federate(_), do: :ok
@doc """
Adds an id and a published data if they aren't there,
also adds it to an included object
"""
@spec lazy_put_activity_defaults(map(), boolean) :: map()
def lazy_put_activity_defaults(map, fake? \\ false)
def lazy_put_activity_defaults(map, true) do
map
|> Map.put_new("id", "pleroma:fakeid")
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", "pleroma:fakecontext")
|> Map.put_new("context_id", -1)
|> lazy_put_object_defaults(true)
end
def lazy_put_activity_defaults(map, _fake?) do
%{data: %{"id" => context}, id: context_id} = create_context(map["context"])
map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", context)
|> Map.put_new("context_id", context_id)
|> lazy_put_object_defaults(false)
end
# Adds an id and published date if they aren't there.
#
@spec lazy_put_object_defaults(map(), boolean()) :: map()
defp lazy_put_object_defaults(%{"object" => map} = activity, true)
when is_map(map) do
object =
map
|> Map.put_new("id", "pleroma:fake_object_id")
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"])
|> Map.put_new("context_id", activity["context_id"])
|> Map.put_new("fake", true)
%{activity | "object" => object}
end
defp lazy_put_object_defaults(%{"object" => map} = activity, _)
when is_map(map) do
object =
map
|> Map.put_new_lazy("id", &generate_object_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"])
|> Map.put_new("context_id", activity["context_id"])
%{activity | "object" => object}
end
defp lazy_put_object_defaults(activity, _), do: activity
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
when is_map(object_data) and type in @supported_object_types do
with {:ok, object} <- Object.create(object_data) do
map = Map.put(map, "object", object.data["id"])
{:ok, map, object}
end
end
def insert_full_object(map), do: {:ok, map, nil}
#### Like-related helpers
@doc """
Returns an existing like if a user already liked an object
"""
@spec get_existing_like(String.t(), map()) :: Activity.t() | nil
def get_existing_like(actor, %{data: %{"id" => id}}) do
actor
|> Activity.Queries.by_actor()
|> Activity.Queries.by_object_id(id)
|> Activity.Queries.by_type("Like")
|> limit(1)
|> Repo.one()
end
@spec make_like_data(User.t(), map(), String.t()) :: map()
def make_like_data(
%User{ap_id: ap_id} = actor,
%{data: %{"actor" => object_actor_id, "id" => id}} = object,
activity_id
) do
object_actor = User.get_cached_by_ap_id(object_actor_id)
to =
if Visibility.is_public?(object) do
[actor.follower_address, object.data["actor"]]
else
[object.data["actor"]]
end
cc =
(object.data["to"] ++ (object.data["cc"] || []))
|> List.delete(actor.ap_id)
|> List.delete(object_actor.follower_address)
%{
"type" => "Like",
"actor" => ap_id,
"object" => id,
"to" => to,
"cc" => cc,
"context" => object.data["context"]
}
|> maybe_put("id", activity_id)
end
@spec update_element_in_object(String.t(), list(any), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def update_element_in_object(property, element, object) do
data =
Map.merge(
object.data,
%{"#{property}_count" => length(element), "#{property}s" => element}
)
object
|> Changeset.change(data: data)
|> Object.update_and_set_cache()
end
@spec add_like_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
[actor | fetch_likes(object)]
|> Enum.uniq()
|> update_likes_in_object(object)
end
@spec remove_like_from_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
object
|> fetch_likes()
|> List.delete(actor)
|> update_likes_in_object(object)
end
defp update_likes_in_object(likes, object) do
update_element_in_object("like", likes, object)
end
defp fetch_likes(object) do
if is_list(object.data["likes"]) do
object.data["likes"]
else
[]
end
end
#### Follow-related helpers
@doc """
Updates a follow activity's state (for locked accounts).
"""
@spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity} | {:error, any()}
def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
"Follow"
|> Activity.Queries.by_type()
|> Activity.Queries.by_actor(actor)
|> Activity.Queries.by_object_id(object)
|> where(fragment("data->>'state' = 'pending'"))
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([])
User.set_follow_state_cache(actor, object, state)
activity = Activity.get_by_id(activity.id)
{:ok, activity}
end
def update_follow_state(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
new_data = Map.put(activity.data, "state", state)
changeset = Changeset.change(activity, data: new_data)
with {:ok, activity} <- Repo.update(changeset) do
User.set_follow_state_cache(actor, object, state)
{:ok, activity}
end
end
@doc """
Makes a follow activity data for the given follower and followed
"""
def make_follow_data(
%User{ap_id: follower_id},
%User{ap_id: followed_id} = _followed,
activity_id
) do
%{
"type" => "Follow",
"actor" => follower_id,
"to" => [followed_id],
"cc" => [Pleroma.Constants.as_public()],
"object" => followed_id,
"state" => "pending"
}
|> maybe_put("id", activity_id)
end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
"Follow"
|> Activity.Queries.by_type()
|> where(actor: ^follower_id)
# this is to use the index
|> Activity.Queries.by_object_id(followed_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
#### Announce-related helpers
@doc """
Retruns an existing announce activity if the notice has already been announced
"""
@spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
"Announce"
|> Activity.Queries.by_type()
|> where(actor: ^actor)
# this is to use the index
|> Activity.Queries.by_object_id(ap_id)
|> Repo.one()
end
@doc """
Make announce activity data for the given actor and object
"""
# for relayed messages, we only want to send to subscribers
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
activity_id,
false
) do
%{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address],
"cc" => [],
"context" => object.data["context"]
}
|> maybe_put("id", activity_id)
end
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
activity_id,
true
) do
%{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => object.data["context"]
}
|> maybe_put("id", activity_id)
end
@doc """
Make unannounce activity data for the given actor and object
"""
def make_unannounce_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context, "object" => object}} = activity,
activity_id
) do
object = Object.normalize(object)
%{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
|> maybe_put("id", activity_id)
end
def make_unlike_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context, "object" => object}} = activity,
activity_id
) do
object = Object.normalize(object)
%{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
|> maybe_put("id", activity_id)
end
@spec add_announce_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_announce_to_object(
%Activity{data: %{"actor" => actor}},
object
) do
- announcements = take_announcements(object)
+ unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
+ announcements = take_announcements(object)
- with announcements <- Enum.uniq([actor | announcements]) do
- update_element_in_object("announcement", announcements, object)
+ with announcements <- Enum.uniq([actor | announcements]) do
+ update_element_in_object("announcement", announcements, object)
+ end
+ else
+ {:ok, object}
end
end
def add_announce_to_object(_, object), do: {:ok, object}
@spec remove_announce_from_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
with announcements <- List.delete(take_announcements(object), actor) do
update_element_in_object("announcement", announcements, object)
end
end
defp take_announcements(%{data: %{"announcements" => announcements}} = _)
when is_list(announcements),
do: announcements
defp take_announcements(_), do: []
#### Unfollow-related helpers
def make_unfollow_data(follower, followed, follow_activity, activity_id) do
%{
"type" => "Undo",
"actor" => follower.ap_id,
"to" => [followed.ap_id],
"object" => follow_activity.data
}
|> maybe_put("id", activity_id)
end
#### Block-related helpers
@spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
"Block"
|> Activity.Queries.by_type()
|> where(actor: ^blocker_id)
# this is to use the index
|> Activity.Queries.by_object_id(blocked_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
def make_block_data(blocker, blocked, activity_id) do
%{
"type" => "Block",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => blocked.ap_id
}
|> maybe_put("id", activity_id)
end
def make_unblock_data(blocker, blocked, block_activity, activity_id) do
%{
"type" => "Undo",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => block_activity.data
}
|> maybe_put("id", activity_id)
end
#### Create-related helpers
def make_create_data(params, additional) do
published = params.published || make_date()
%{
"type" => "Create",
"to" => params.to |> Enum.uniq(),
"actor" => params.actor.ap_id,
"object" => params.object,
"published" => published,
"context" => params.context
}
|> Map.merge(additional)
end
#### Listen-related helpers
def make_listen_data(params, additional) do
published = params.published || make_date()
%{
"type" => "Listen",
"to" => params.to |> Enum.uniq(),
"actor" => params.actor.ap_id,
"object" => params.object,
"published" => published,
"context" => params.context
}
|> Map.merge(additional)
end
#### Flag-related helpers
@spec make_flag_data(map(), map()) :: map()
def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
%{
"type" => "Flag",
"actor" => actor.ap_id,
"content" => content,
"object" => build_flag_object(params),
"context" => context,
"state" => "open"
}
|> Map.merge(additional)
end
def make_flag_data(_, _), do: %{}
defp build_flag_object(%{account: account, statuses: statuses} = _) do
[account.ap_id] ++
Enum.map(statuses || [], fn act ->
id =
case act do
%Activity{} = act -> act.data["id"]
act when is_map(act) -> act["id"]
act when is_binary(act) -> act
end
activity = Activity.get_by_ap_id_with_object(id)
actor = User.get_by_ap_id(activity.object.data["actor"])
%{
"type" => "Note",
"id" => activity.data["id"],
"content" => activity.object.data["content"],
"published" => activity.object.data["published"],
"actor" => AccountView.render("show.json", %{user: actor})
}
end)
end
defp build_flag_object(_), do: []
@doc """
Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
the first one to `pages_left` pages.
If the amount of pages is higher than the collection has, it returns whatever was there.
"""
def fetch_ordered_collection(from, pages_left, acc \\ []) do
with {:ok, response} <- Tesla.get(from),
{:ok, collection} <- Jason.decode(response.body) do
case collection["type"] do
"OrderedCollection" ->
# If we've encountered the OrderedCollection and not the page,
# just call the same function on the page address
fetch_ordered_collection(collection["first"], pages_left)
"OrderedCollectionPage" ->
if pages_left > 0 do
# There are still more pages
if Map.has_key?(collection, "next") do
# There are still more pages, go deeper saving what we have into the accumulator
fetch_ordered_collection(
collection["next"],
pages_left - 1,
acc ++ collection["orderedItems"]
)
else
# No more pages left, just return whatever we already have
acc ++ collection["orderedItems"]
end
else
# Got the amount of pages needed, add them all to the accumulator
acc ++ collection["orderedItems"]
end
_ ->
{:error, "Not an OrderedCollection or OrderedCollectionPage"}
end
end
end
#### Report-related helpers
def update_report_state(%Activity{} = activity, state)
when state in @strip_status_report_states do
{:ok, stripped_activity} = strip_report_status_data(activity)
new_data =
activity.data
|> Map.put("state", state)
|> Map.put("object", stripped_activity.data["object"])
activity
|> Changeset.change(data: new_data)
|> Repo.update()
end
def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
new_data = Map.put(activity.data, "state", state)
activity
|> Changeset.change(data: new_data)
|> Repo.update()
end
def update_report_state(_, _), do: {:error, "Unsupported state"}
def strip_report_status_data(activity) do
[actor | reported_activities] = activity.data["object"]
stripped_activities = Enum.map(reported_activities, & &1["id"])
new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
{:ok, %{activity | data: new_data}}
end
def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
[to, cc, recipients] =
activity
|> get_updated_targets(visibility)
|> Enum.map(&Enum.uniq/1)
object_data =
activity.object.data
|> Map.put("to", to)
|> Map.put("cc", cc)
{:ok, object} =
activity.object
|> Object.change(%{data: object_data})
|> Object.update_and_set_cache()
activity_data =
activity.data
|> Map.put("to", to)
|> Map.put("cc", cc)
activity
|> Map.put(:object, object)
|> Activity.change(%{data: activity_data, recipients: recipients})
|> Repo.update()
end
def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
defp get_updated_targets(
%Activity{data: %{"to" => to} = data, recipients: recipients},
visibility
) do
cc = Map.get(data, "cc", [])
follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
public = Pleroma.Constants.as_public()
case visibility do
"public" ->
to = [public | List.delete(to, follower_address)]
cc = [follower_address | List.delete(cc, public)]
recipients = [public | recipients]
[to, cc, recipients]
"private" ->
to = [follower_address | List.delete(to, public)]
cc = List.delete(cc, public)
recipients = List.delete(recipients, public)
[to, cc, recipients]
"unlisted" ->
to = [follower_address | List.delete(to, public)]
cc = [public | List.delete(cc, follower_address)]
recipients = recipients ++ [follower_address, public]
[to, cc, recipients]
_ ->
[to, cc, recipients]
end
end
def get_existing_votes(actor, %{data: %{"id" => id}}) do
actor
|> Activity.Queries.by_actor()
|> Activity.Queries.by_type("Create")
|> Activity.with_preloaded_object()
|> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
|> Repo.all()
end
def maybe_put(map, _key, nil), do: map
def maybe_put(map, key, value), do: Map.put(map, key, value)
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 9b39d1629..cf08045c9 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -1,286 +1,287 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view
alias Pleroma.Keys
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers
import Ecto.Query
def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do
%{"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)}
end
def render("endpoints.json", %{user: %User{local: true} = _user}) do
%{
"oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
"oauthRegistrationEndpoint" => Helpers.app_url(Endpoint, :create),
"oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox),
"uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media)
}
end
def render("endpoints.json", _), do: %{}
def render("service.json", %{user: user}) do
{:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
endpoints = render("endpoints.json", %{user: user})
%{
"id" => user.ap_id,
"type" => "Application",
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"name" => "Pleroma",
"summary" =>
"An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
"url" => user.ap_id,
"manuallyApprovesFollowers" => false,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
- "endpoints" => endpoints
+ "endpoints" => endpoints,
+ "invisible" => User.invisible?(user)
}
|> Map.merge(Utils.make_json_ld_header())
end
# the instance itself is not a Person, but instead an Application
def render("user.json", %{user: %User{nickname: nil} = user}),
do: render("service.json", %{user: user})
def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
def render("user.json", %{user: user}) do
{:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
endpoints = render("endpoints.json", %{user: user})
emoji_tags = Transmogrifier.take_emoji_tags(user)
fields =
- user.info
- |> User.Info.fields()
+ user
+ |> User.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => Pleroma.HTML.strip_tags(name),
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
|> Enum.map(&Map.put(&1, "type", "PropertyValue"))
%{
"id" => user.ap_id,
"type" => "Person",
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"outbox" => "#{user.ap_id}/outbox",
"preferredUsername" => user.nickname,
"name" => user.name,
"summary" => user.bio,
"url" => user.ap_id,
- "manuallyApprovesFollowers" => user.info.locked,
+ "manuallyApprovesFollowers" => user.locked,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
"attachment" => fields,
- "tag" => (user.info.source_data["tag"] || []) ++ emoji_tags,
- "discoverable" => user.info.discoverable
+ "tag" => (user.source_data["tag"] || []) ++ emoji_tags,
+ "discoverable" => user.discoverable
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user, page: page} = opts) do
- showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_follows
- showing_count = showing_items || !user.info.hide_follows_count
+ showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
+ showing_count = showing_items || !user.hide_follows_count
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
total =
if showing_count do
length(following)
else
0
end
collection(following, "#{user.ap_id}/following", page, showing_items, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user} = opts) do
- showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_follows
- showing_count = showing_items || !user.info.hide_follows_count
+ showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
+ showing_count = showing_items || !user.hide_follows_count
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
total =
if showing_count do
length(following)
else
0
end
%{
"id" => "#{user.ap_id}/following",
"type" => "OrderedCollection",
"totalItems" => total,
"first" =>
if showing_items do
- collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows)
+ collection(following, "#{user.ap_id}/following", 1, !user.hide_follows)
else
"#{user.ap_id}/following?page=1"
end
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user, page: page} = opts) do
- showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_followers
- showing_count = showing_items || !user.info.hide_followers_count
+ showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers
+ showing_count = showing_items || !user.hide_followers_count
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
total =
if showing_count do
length(followers)
else
0
end
collection(followers, "#{user.ap_id}/followers", page, showing_items, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user} = opts) do
- showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_followers
- showing_count = showing_items || !user.info.hide_followers_count
+ showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers
+ showing_count = showing_items || !user.hide_followers_count
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
total =
if showing_count do
length(followers)
else
0
end
%{
"id" => "#{user.ap_id}/followers",
"type" => "OrderedCollection",
"totalItems" => total,
"first" =>
if showing_items do
collection(followers, "#{user.ap_id}/followers", 1, showing_items, total)
else
"#{user.ap_id}/followers?page=1"
end
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("activity_collection.json", %{iri: iri}) do
%{
"id" => iri,
"type" => "OrderedCollection",
"first" => "#{iri}?page=true"
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("activity_collection_page.json", %{activities: activities, iri: iri}) do
# this is sorted chronologically, so first activity is the newest (max)
{max_id, min_id, collection} =
if length(activities) > 0 do
{
Enum.at(activities, 0).id,
Enum.at(Enum.reverse(activities), 0).id,
Enum.map(activities, fn act ->
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
data
end)
}
else
{
0,
0,
[]
}
end
%{
"id" => "#{iri}?max_id=#{max_id}&page=true",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id}&page=true"
}
|> Map.merge(Utils.make_json_ld_header())
end
def collection(collection, iri, page, show_items \\ true, total \\ nil) do
offset = (page - 1) * 10
items = Enum.slice(collection, offset, 10)
items = Enum.map(items, fn user -> user.ap_id end)
total = total || length(collection)
map = %{
"id" => "#{iri}?page=#{page}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"totalItems" => total,
"orderedItems" => if(show_items, do: items, else: [])
}
if offset < total do
Map.put(map, "next", "#{iri}?page=#{page + 1}")
else
map
end
end
defp maybe_make_image(func, key, user) do
if image = func.(user, no_default: true) do
%{
key => %{
"type" => "Image",
"url" => image
}
}
else
%{}
end
end
end
diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex
index 270d0fa02..f3ab48f7c 100644
--- a/lib/pleroma/web/activity_pub/visibility.ex
+++ b/lib/pleroma/web/activity_pub/visibility.ex
@@ -1,104 +1,105 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Visibility do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Utils
require Pleroma.Constants
@spec is_public?(Object.t() | Activity.t() | map()) :: boolean()
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
def is_public?(%Object{data: data}), do: is_public?(data)
def is_public?(%Activity{data: data}), do: is_public?(data)
def is_public?(%{"directMessage" => true}), do: false
- def is_public?(data), do: Pleroma.Constants.as_public() in (data["to"] ++ (data["cc"] || []))
+ def is_public?(data), do: Utils.label_in_message?(Pleroma.Constants.as_public(), data)
def is_private?(activity) do
with false <- is_public?(activity),
%User{follower_address: follower_address} <-
User.get_cached_by_ap_id(activity.data["actor"]) do
follower_address in activity.data["to"]
else
_ -> false
end
end
def is_announceable?(activity, user, public \\ true) do
is_public?(activity) ||
(!public && is_private?(activity) && activity.data["actor"] == user.ap_id)
end
def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
def is_direct?(%Object{data: %{"directMessage" => true}}), do: true
def is_direct?(activity) do
!is_public?(activity) && !is_private?(activity)
end
def is_list?(%{data: %{"listMessage" => _}}), do: true
def is_list?(_), do: false
def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true
def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do
user.ap_id in activity.data["to"] ||
list_ap_id
|> Pleroma.List.get_by_ap_id()
|> Pleroma.List.member?(user)
end
def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
def visible_for_user?(activity, nil) do
is_public?(activity)
end
def visible_for_user?(activity, user) do
x = [user.ap_id | user.following]
y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || [])
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
{:ok, %{rows: [[result]]}} =
Ecto.Adapters.SQL.query(Repo, "SELECT thread_visibility($1, $2)", [
user.ap_id,
activity.data["id"]
])
result
end
def get_visibility(object) do
to = object.data["to"] || []
cc = object.data["cc"] || []
cond do
Pleroma.Constants.as_public() in to ->
"public"
Pleroma.Constants.as_public() in cc ->
"unlisted"
# this should use the sql for the object's activity
Enum.any?(to, &String.contains?(&1, "/followers")) ->
"private"
object.data["directMessage"] == true ->
"direct"
is_binary(object.data["listMessage"]) ->
"list"
length(cc) > 0 ->
"private"
true ->
"direct"
end
end
end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 513bae800..7ffbb23e7 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -1,732 +1,821 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Router
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
require Logger
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
when action in [:list_users, :user_show, :right_get, :invites]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"]}
when action in [
:get_invite_token,
:revoke_invite,
:email_invite,
:get_password_reset,
:user_follow,
:user_unfollow,
:user_delete,
:users_create,
:user_toggle_activation,
+ :user_activate,
+ :user_deactivate,
:tag_users,
:untag_users,
:right_add,
- :right_delete,
- :set_activation_status
+ :right_delete
]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:reports"]} when action in [:list_reports, :report_show]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:reports"]}
when action in [:report_update_state, :report_respond]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"]} when action == :list_user_statuses
)
plug(
OAuthScopesPlug,
%{scopes: ["write:statuses"]}
when action in [:status_update, :status_delete]
)
plug(
OAuthScopesPlug,
%{scopes: ["read"]}
when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
)
plug(
OAuthScopesPlug,
%{scopes: ["write"]}
when action in [:relay_follow, :relay_unfollow, :config_update]
)
@users_page_size 50
action_fallback(:errors)
def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
User.delete(user)
ModerationLog.insert_log(%{
actor: admin,
- subject: user,
+ subject: [user],
action: "delete"
})
conn
|> json(nickname)
end
+ def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+ users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
+ User.delete(users)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "delete"
+ })
+
+ conn
+ |> json(nicknames)
+ end
+
def user_follow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick,
"followed" => followed_nick
}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.follow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "follow"
})
end
conn
|> json("ok")
end
def user_unfollow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick,
"followed" => followed_nick
}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.unfollow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "unfollow"
})
end
conn
|> json("ok")
end
def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
changesets =
Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
user_data = %{
nickname: nickname,
name: nickname,
email: email,
password: password,
password_confirmation: password,
bio: "."
}
User.register_changeset(%User{}, user_data, need_confirmation: false)
end)
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
end)
case Pleroma.Repo.transaction(changesets) do
{:ok, users} ->
res =
users
|> Map.values()
|> Enum.map(fn user ->
{:ok, user} = User.post_register_action(user)
user
end)
|> Enum.map(&AccountView.render("created.json", %{user: &1}))
ModerationLog.insert_log(%{
actor: admin,
subjects: Map.values(users),
action: "create"
})
conn
|> json(res)
{:error, id, changeset, _} ->
res =
Enum.map(changesets.operations, fn
{current_id, {:changeset, _current_changeset, _}} when current_id == id ->
AccountView.render("create-error.json", %{changeset: changeset})
{_, {:changeset, current_changeset, _}} ->
AccountView.render("create-error.json", %{changeset: current_changeset})
end)
conn
|> put_status(:conflict)
|> json(res)
end
end
def user_show(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
conn
|> put_view(AccountView)
|> render("show.json", %{user: user})
else
_ -> {:error, :not_found}
end
end
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
godmode = params["godmode"] == "true" || params["godmode"] == true
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
{_, page_size} = page_params(params)
activities =
ActivityPub.fetch_user_activities(user, nil, %{
"limit" => page_size,
"godmode" => godmode
})
conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, as: :activity})
else
_ -> {:error, :not_found}
end
end
def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
- {:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
+ {:ok, updated_user} = User.deactivate(user, !user.deactivated)
- action = if user.info.deactivated, do: "activate", else: "deactivate"
+ action = if user.deactivated, do: "activate", else: "deactivate"
ModerationLog.insert_log(%{
actor: admin,
- subject: user,
+ subject: [user],
action: action
})
conn
|> put_view(AccountView)
|> render("show.json", %{user: updated_user})
end
+ def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+ {:ok, updated_users} = User.deactivate(users, false)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "activate"
+ })
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", %{users: Keyword.values(updated_users)})
+ end
+
+ def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+ users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+ {:ok, updated_users} = User.deactivate(users, true)
+
+ ModerationLog.insert_log(%{
+ actor: admin,
+ subject: users,
+ action: "deactivate"
+ })
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", %{users: Keyword.values(updated_users)})
+ end
+
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags) do
ModerationLog.insert_log(%{
actor: admin,
nicknames: nicknames,
tags: tags,
action: "tag"
})
json_response(conn, :no_content, "")
end
end
def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.untag(nicknames, tags) do
ModerationLog.insert_log(%{
actor: admin,
nicknames: nicknames,
tags: tags,
action: "untag"
})
json_response(conn, :no_content, "")
end
end
def list_users(conn, params) do
{page, page_size} = page_params(params)
filters = maybe_parse_filters(params["filters"])
search_params = %{
query: params["query"],
page: page,
page_size: page_size,
tags: params["tags"],
name: params["name"],
email: params["email"]
}
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
do:
conn
|> json(
AccountView.render("index.json",
users: users,
count: count,
page_size: page_size
)
)
end
@filters ~w(local external active deactivated is_admin is_moderator)
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
defp maybe_parse_filters(filters) do
filters
|> String.split(",")
|> Enum.filter(&Enum.member?(@filters, &1))
|> Enum.map(&String.to_atom(&1))
|> Enum.into(%{}, &{&1, true})
end
+ def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
+ "permission_group" => permission_group,
+ "nicknames" => nicknames
+ })
+ when permission_group in ["moderator", "admin"] do
+ update = %{:"is_#{permission_group}" => true}
+
+ users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
+
+ for u <- users, do: User.admin_api_update(u, update)
+
+ ModerationLog.insert_log(%{
+ action: "grant",
+ actor: admin,
+ subject: users,
+ permission: permission_group
+ })
+
+ json(conn, update)
+ end
+
+ def right_add_multiple(conn, _) do
+ render_error(conn, :not_found, "No such permission_group")
+ end
+
def right_add(%{assigns: %{user: admin}} = conn, %{
"permission_group" => permission_group,
"nickname" => nickname
})
when permission_group in ["moderator", "admin"] do
- info = Map.put(%{}, "is_" <> permission_group, true)
+ fields = %{:"is_#{permission_group}" => true}
{:ok, user} =
nickname
|> User.get_cached_by_nickname()
- |> User.update_info(&User.Info.admin_api_update(&1, info))
+ |> User.admin_api_update(fields)
ModerationLog.insert_log(%{
action: "grant",
actor: admin,
- subject: user,
+ subject: [user],
permission: permission_group
})
- json(conn, info)
+ json(conn, fields)
end
def right_add(conn, _) do
render_error(conn, :not_found, "No such permission_group")
end
def right_get(conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
conn
|> json(%{
- is_moderator: user.info.is_moderator,
- is_admin: user.info.is_admin
+ is_moderator: user.is_moderator,
+ is_admin: user.is_admin
})
end
- def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
- render_error(conn, :forbidden, "You can't revoke your own admin status.")
+ def right_delete_multiple(
+ %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
+ %{
+ "permission_group" => permission_group,
+ "nicknames" => nicknames
+ }
+ )
+ when permission_group in ["moderator", "admin"] do
+ with false <- Enum.member?(nicknames, admin_nickname) do
+ update = %{:"is_#{permission_group}" => false}
+
+ users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
+
+ for u <- users, do: User.admin_api_update(u, update)
+
+ ModerationLog.insert_log(%{
+ action: "revoke",
+ actor: admin,
+ subject: users,
+ permission: permission_group
+ })
+
+ json(conn, update)
+ else
+ _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
+ end
+ end
+
+ def right_delete_multiple(conn, _) do
+ render_error(conn, :not_found, "No such permission_group")
end
def right_delete(
%{assigns: %{user: admin}} = conn,
%{
"permission_group" => permission_group,
"nickname" => nickname
}
)
when permission_group in ["moderator", "admin"] do
- info = Map.put(%{}, "is_" <> permission_group, false)
+ fields = %{:"is_#{permission_group}" => false}
{:ok, user} =
nickname
|> User.get_cached_by_nickname()
- |> User.update_info(&User.Info.admin_api_update(&1, info))
+ |> User.admin_api_update(fields)
ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
- subject: user,
+ subject: [user],
permission: permission_group
})
- json(conn, info)
+ json(conn, fields)
end
- def right_delete(conn, _) do
- render_error(conn, :not_found, "No such permission_group")
+ def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
+ render_error(conn, :forbidden, "You can't revoke your own admin status.")
end
- def set_activation_status(%{assigns: %{user: admin}} = conn, %{
- "nickname" => nickname,
- "status" => status
- }) do
- with {:ok, status} <- Ecto.Type.cast(:boolean, status),
- %User{} = user <- User.get_cached_by_nickname(nickname),
- {:ok, _} <- User.deactivate(user, !status) do
- action = if(user.info.deactivated, do: "activate", else: "deactivate")
-
- ModerationLog.insert_log(%{
- actor: admin,
- subject: user,
- action: action
- })
-
- json_response(conn, :no_content, "")
+ def relay_list(conn, _params) do
+ with {:ok, list} <- Relay.list() do
+ json(conn, %{relays: list})
+ else
+ _ ->
+ conn
+ |> put_status(500)
end
end
def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
with {:ok, _message} <- Relay.follow(target) do
ModerationLog.insert_log(%{
action: "relay_follow",
actor: admin,
target: target
})
json(conn, target)
else
_ ->
conn
|> put_status(500)
|> json(target)
end
end
def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
with {:ok, _message} <- Relay.unfollow(target) do
ModerationLog.insert_log(%{
action: "relay_unfollow",
actor: admin,
target: target
})
json(conn, target)
else
_ ->
conn
|> put_status(500)
|> json(target)
end
end
@doc "Sends registration invite via email"
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
with true <-
Pleroma.Config.get([:instance, :invites_enabled]) &&
!Pleroma.Config.get([:instance, :registrations_open]),
{:ok, invite_token} <- UserInviteToken.create_invite(),
email <-
Pleroma.Emails.UserEmail.user_invitation_email(
user,
invite_token,
email,
params["name"]
),
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
json_response(conn, :no_content, "")
end
end
@doc "Create an account registration invite token"
def create_invite_token(conn, params) do
opts = %{}
opts =
if params["max_use"],
do: Map.put(opts, :max_use, params["max_use"]),
else: opts
opts =
if params["expires_at"],
do: Map.put(opts, :expires_at, params["expires_at"]),
else: opts
{:ok, invite} = UserInviteToken.create_invite(opts)
json(conn, AccountView.render("invite.json", %{invite: invite}))
end
@doc "Get list of created invites"
def invites(conn, _params) do
invites = UserInviteToken.list_invites()
conn
|> put_view(AccountView)
|> render("invites.json", %{invites: invites})
end
@doc "Revokes invite by token"
def revoke_invite(conn, %{"token" => token}) do
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
conn
|> put_view(AccountView)
|> render("invite.json", %{invite: updated_invite})
else
nil -> {:error, :not_found}
end
end
@doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
conn
|> json(%{
token: token.token,
link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
})
end
@doc "Force password reset for a given user"
def force_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
User.force_password_reset_async(user)
json_response(conn, :no_content, "")
end
def list_reports(conn, params) do
{page, page_size} = page_params(params)
params =
params
|> Map.put("type", "Flag")
|> Map.put("skip_preload", true)
|> Map.put("total", true)
|> Map.put("limit", page_size)
|> Map.put("offset", (page - 1) * page_size)
reports = ActivityPub.fetch_activities([], params, :offset)
conn
|> put_view(ReportView)
|> render("index.json", %{reports: reports})
end
def report_show(conn, %{"id" => id}) do
with %Activity{} = report <- Activity.get_by_id(id) do
conn
|> put_view(ReportView)
|> render("show.json", Report.extract_report_info(report))
else
_ -> {:error, :not_found}
end
end
def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
with {:ok, report} <- CommonAPI.update_report_state(id, state) do
ModerationLog.insert_log(%{
action: "report_update",
actor: admin,
subject: report
})
conn
|> put_view(ReportView)
|> render("show.json", Report.extract_report_info(report))
end
end
def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
with false <- is_nil(params["status"]),
%Activity{} <- Activity.get_by_id(id) do
params =
params
|> Map.put("in_reply_to_status_id", id)
|> Map.put("visibility", "direct")
{:ok, activity} = CommonAPI.post(user, params)
ModerationLog.insert_log(%{
action: "report_response",
actor: user,
subject: activity,
text: params["status"]
})
conn
|> put_view(StatusView)
|> render("show.json", %{activity: activity})
else
true ->
{:param_cast, nil}
nil ->
{:error, :not_found}
end
end
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
ModerationLog.insert_log(%{
action: "status_update",
actor: admin,
subject: activity,
sensitive: sensitive,
visibility: params["visibility"]
})
conn
|> put_view(StatusView)
|> render("show.json", %{activity: activity})
end
end
def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
ModerationLog.insert_log(%{
action: "status_delete",
actor: user,
subject_id: id
})
json(conn, %{})
end
end
def list_log(conn, params) do
{page, page_size} = page_params(params)
log =
ModerationLog.get_all(%{
page: page,
page_size: page_size,
start_date: params["start_date"],
end_date: params["end_date"],
user_id: params["user_id"],
search: params["search"]
})
conn
|> put_view(ModerationLogView)
|> render("index.json", %{log: log})
end
def migrate_to_db(conn, _params) do
Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
json(conn, %{})
end
def migrate_from_db(conn, _params) do
Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
json(conn, %{})
end
def config_show(conn, _params) do
configs = Pleroma.Repo.all(Config)
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: configs})
end
def config_update(conn, %{"configs" => configs}) do
updated =
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
updated =
Enum.map(configs, fn
%{"group" => group, "key" => key, "delete" => "true"} = params ->
{:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
config
%{"group" => group, "key" => key, "value" => value} ->
{:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
config
end)
|> Enum.reject(&is_nil(&1))
Pleroma.Config.TransferTask.load_and_update_env()
Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
updated
else
[]
end
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: updated})
end
def reload_emoji(conn, _params) do
Pleroma.Emoji.reload()
conn |> json("ok")
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
def errors(conn, {:error, reason}) do
conn
|> put_status(:bad_request)
|> json(reason)
end
def errors(conn, {:param_cast, _}) do
conn
|> put_status(:bad_request)
|> json(dgettext("errors", "Invalid parameters"))
end
def errors(conn, _) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "Something went wrong"))
end
defp page_params(params) do
{get_page(params["page"]), get_page_size(params["page_size"])}
end
defp get_page(page_string) when is_nil(page_string), do: 1
defp get_page(page_string) do
case Integer.parse(page_string) do
{page, _} -> page
:error -> 1
end
end
defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
defp get_page_size(page_size_string) do
case Integer.parse(page_size_string) do
{page_size, _} -> page_size
:error -> @users_page_size
end
end
end
diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex
index a96affd40..6aa7257ce 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -1,101 +1,106 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AccountView do
use Pleroma.Web, :view
alias Pleroma.HTML
alias Pleroma.User
- alias Pleroma.User.Info
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.MediaProxy
def render("index.json", %{users: users, count: count, page_size: page_size}) do
%{
users: render_many(users, AccountView, "show.json", as: :user),
count: count,
page_size: page_size
}
end
+ def render("index.json", %{users: users}) do
+ %{
+ users: render_many(users, AccountView, "show.json", as: :user)
+ }
+ end
+
def render("show.json", %{user: user}) do
avatar = User.avatar_url(user) |> MediaProxy.url()
display_name = HTML.strip_tags(user.name || user.nickname)
%{
"id" => user.id,
"avatar" => avatar,
"nickname" => user.nickname,
"display_name" => display_name,
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"local" => user.local,
- "roles" => Info.roles(user.info),
+ "roles" => User.roles(user),
"tags" => user.tags || []
}
end
def render("invite.json", %{invite: invite}) do
%{
"id" => invite.id,
"token" => invite.token,
"used" => invite.used,
"expires_at" => invite.expires_at,
"uses" => invite.uses,
"max_use" => invite.max_use,
"invite_type" => invite.invite_type
}
end
def render("invites.json", %{invites: invites}) do
%{
invites: render_many(invites, AccountView, "invite.json", as: :invite)
}
end
def render("created.json", %{user: user}) do
%{
type: "success",
code: 200,
data: %{
nickname: user.nickname,
email: user.email
}
}
end
def render("create-error.json", %{changeset: %Ecto.Changeset{changes: changes, errors: errors}}) do
%{
type: "error",
code: 409,
error: parse_error(errors),
data: %{
nickname: Map.get(changes, :nickname),
email: Map.get(changes, :email)
}
}
end
defp parse_error([]), do: ""
defp parse_error(errors) do
## when nickname is duplicate ap_id constraint error is raised
nickname_error = Keyword.get(errors, :nickname) || Keyword.get(errors, :ap_id)
email_error = Keyword.get(errors, :email)
password_error = Keyword.get(errors, :password)
cond do
nickname_error ->
"nickname #{elem(nickname_error, 0)}"
email_error ->
"email #{elem(email_error, 0)}"
password_error ->
"password #{elem(password_error, 0)}"
true ->
""
end
end
end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 386408d51..449b808b5 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -1,405 +1,405 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
import Pleroma.Web.Gettext
import Pleroma.Web.CommonAPI.Utils
require Pleroma.Constants
def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
{:ok, follower, followed, activity}
end
end
def unfollow(follower, unfollowed) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
{:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
{:ok, follower}
end
end
def accept_follow_request(follower, followed) do
with {:ok, follower} <- User.follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
object: follow_activity.data["id"],
type: "Accept"
}) do
{:ok, follower}
end
end
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: follow_activity.data["id"],
type: "Reject"
}) do
{:ok, follower}
end
end
def delete(activity_id, user) do
with %Activity{data: %{"object" => _}} = activity <-
Activity.get_by_id_with_object(activity_id),
%Object{} = object <- Object.normalize(activity),
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
{:ok, _} <- unpin(activity_id, user),
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
else
_ -> {:error, dgettext("errors", "Could not delete")}
end
end
def repeat(id_or_ap_id, user, params \\ %{}) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity),
nil <- Utils.get_existing_announce(user.ap_id, object),
public <- public_announce?(object, params) do
ActivityPub.announce(user, object, nil, true, public)
else
_ -> {:error, dgettext("errors", "Could not repeat")}
end
end
def unrepeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
object = Object.normalize(activity)
ActivityPub.unannounce(user, object)
else
_ -> {:error, dgettext("errors", "Could not unrepeat")}
end
end
def favorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity),
nil <- Utils.get_existing_like(user.ap_id, object) do
ActivityPub.like(user, object)
else
_ -> {:error, dgettext("errors", "Could not favorite")}
end
end
def unfavorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
object = Object.normalize(activity)
ActivityPub.unlike(user, object)
else
_ -> {:error, dgettext("errors", "Could not unfavorite")}
end
end
def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
with :ok <- validate_not_author(object, user),
:ok <- validate_existing_votes(user, object),
{:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
answer_activities =
Enum.map(choices, fn index ->
answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
{:ok, activity} =
ActivityPub.create(%{
to: answer_data["to"],
actor: user,
context: object.data["context"],
object: answer_data,
additional: %{"cc" => answer_data["cc"]}
})
activity
end)
object = Object.get_cached_by_ap_id(object.data["id"])
{:ok, answer_activities, object}
end
end
defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
do: {:error, dgettext("errors", "Poll's author can't vote")}
defp validate_not_author(_, _), do: :ok
defp validate_existing_votes(%{ap_id: ap_id}, object) do
if Utils.get_existing_votes(ap_id, object) == [] do
:ok
else
{:error, dgettext("errors", "Already voted")}
end
end
defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
defp normalize_and_validate_choices(choices, object) do
choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
{options, max_count} = get_options_and_max_count(object)
count = Enum.count(options)
with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
{_, true} <- {:count_check, Enum.count(choices) <= max_count} do
{:ok, options, choices}
else
{:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
{:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
end
end
def public_announce?(_, %{"visibility" => visibility})
when visibility in ~w{public unlisted private direct},
do: visibility in ~w(public unlisted)
def public_announce?(object, _) do
Visibility.is_public?(object)
end
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
visibility = {:list, String.to_integer(list_id)}
{visibility, get_replied_to_visibility(in_reply_to)}
end
def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
visibility = get_replied_to_visibility(in_reply_to)
{visibility, visibility}
end
def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
def get_replied_to_visibility(nil), do: nil
def get_replied_to_visibility(activity) do
with %Object{} = object <- Object.normalize(activity) do
Visibility.get_visibility(object)
end
end
def check_expiry_date({:ok, nil} = res), do: res
def check_expiry_date({:ok, in_seconds}) do
expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
if ActivityExpiration.expires_late_enough?(expiry) do
{:ok, expiry}
else
{:error, "Expiry date is too soon"}
end
end
def check_expiry_date(expiry_str) do
Ecto.Type.cast(:integer, expiry_str)
|> check_expiry_date()
end
def listen(user, %{"title" => _} = data) do
with visibility <- data["visibility"] || "public",
{to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
listen_data <-
Map.take(data, ["album", "artist", "title", "length"])
|> Map.put("type", "Audio")
|> Map.put("to", to)
|> Map.put("cc", cc)
|> Map.put("actor", user.ap_id),
{:ok, activity} <-
ActivityPub.listen(%{
actor: user,
to: to,
object: listen_data,
context: Utils.generate_context_id(),
additional: %{"cc" => cc}
}) do
{:ok, activity}
end
end
def post(user, %{"status" => _} = data) do
with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
draft.changes
|> ActivityPub.create(draft.preview?)
|> maybe_create_activity_expiration(draft.expires_at)
end
end
defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
{:ok, activity}
end
end
defp maybe_create_activity_expiration(result, _), do: result
# Updates the emojis for a user based on their profile
def update(user) do
emoji = emoji_from_profile(user)
- source_data = user.info |> Map.get(:source_data, %{}) |> Map.put("tag", emoji)
+ source_data = Map.put(user.source_data, "tag", emoji)
user =
- case User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
+ case User.update_source_data(user, source_data) do
{:ok, user} -> user
_ -> user
end
ActivityPub.update(%{
local: true,
to: [Pleroma.Constants.as_public(), user.follower_address],
cc: [],
actor: user.ap_id,
object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
})
end
def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,
data: %{"type" => "Create"},
object: %Object{data: %{"type" => "Note"}}
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
true <- Visibility.is_public?(activity),
- {:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
+ {:ok, _user} <- User.add_pinnned_activity(user, activity) do
{:ok, activity}
else
- {:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err}
+ {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
_ -> {:error, dgettext("errors", "Could not pin")}
end
end
def unpin(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
- {:ok, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do
+ {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
{:ok, activity}
else
- %{errors: [pinned_activities: {err, _}]} -> {:error, err}
+ {:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
_ -> {:error, dgettext("errors", "Could not unpin")}
end
end
def add_mute(user, activity) do
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
{:ok, activity}
else
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
end
end
def remove_mute(user, activity) do
ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity}
end
def thread_muted?(%{id: nil} = _user, _activity), do: false
def thread_muted?(user, activity) do
ThreadMute.check_muted(user.id, activity.data["context"]) != []
end
def report(user, %{"account_id" => account_id} = data) do
with {:ok, account} <- get_reported_account(account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
{:ok, statuses} <- get_report_statuses(account, data) do
ActivityPub.flag(%{
context: Utils.generate_context_id(),
actor: user,
account: account,
statuses: statuses,
content: content_html,
forward: data["forward"] || false
})
end
end
def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
defp get_reported_account(account_id) do
case User.get_cached_by_id(account_id) do
%User{} = account -> {:ok, account}
_ -> {:error, dgettext("errors", "Account not found")}
end
end
def update_report_state(activity_id, state) do
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
Utils.update_report_state(activity, state)
else
nil -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end
def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
{:ok, activity} <- toggle_sensitive(activity, opts) do
set_visibility(activity, opts)
else
nil -> {:error, :not_found}
{:error, reason} -> {:error, reason}
end
end
defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
end
defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
when is_boolean(sensitive) do
new_data = Map.put(object.data, "sensitive", sensitive)
{:ok, object} =
object
|> Object.change(%{data: new_data})
|> Object.update_and_set_cache()
{:ok, Map.put(activity, :object, object)}
end
defp toggle_sensitive(activity, _), do: {:ok, activity}
defp set_visibility(activity, %{"visibility" => visibility}) do
Utils.update_activity_visibility(activity, visibility)
end
defp set_visibility(activity, _), do: {:ok, activity}
def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
- if ap_id not in user.info.muted_reblogs do
- User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id))
+ if ap_id not in user.muted_reblogs do
+ User.add_reblog_mute(user, ap_id)
end
end
def show_reblogs(user, %{ap_id: ap_id} = _muted) do
- if ap_id in user.info.muted_reblogs do
- User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id))
+ if ap_id in user.muted_reblogs do
+ User.remove_reblog_mute(user, ap_id)
end
end
end
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index 1a2da014a..e8a56ebd7 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -1,146 +1,94 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator do
alias Pleroma.Activity
alias Pleroma.Object.Containment
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator.Publisher
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.Websub
alias Pleroma.Workers.PublisherWorker
alias Pleroma.Workers.ReceiverWorker
- alias Pleroma.Workers.SubscriberWorker
require Logger
- def init do
- # To do: consider removing this call in favor of scheduled execution (`quantum`-based)
- refresh_subscriptions(schedule_in: 60)
- end
-
@doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)"
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
def allowed_incoming_reply_depth?(depth) do
max_replies_depth = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth])
if max_replies_depth do
(depth || 1) <= max_replies_depth
else
true
end
end
# Client API
- def incoming_doc(doc) do
- ReceiverWorker.enqueue("incoming_doc", %{"body" => doc})
- end
-
def incoming_ap_doc(params) do
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params})
end
def publish(%{id: "pleroma:fakeid"} = activity) do
perform(:publish, activity)
end
def publish(activity) do
PublisherWorker.enqueue("publish", %{"activity_id" => activity.id})
end
- def verify_websub(websub) do
- SubscriberWorker.enqueue("verify_websub", %{"websub_id" => websub.id})
- end
-
- def request_subscription(websub) do
- SubscriberWorker.enqueue("request_subscription", %{"websub_id" => websub.id})
- end
-
- def refresh_subscriptions(worker_args \\ []) do
- SubscriberWorker.enqueue("refresh_subscriptions", %{}, worker_args ++ [max_attempts: 1])
- end
-
# Job Worker Callbacks
@spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
def perform(:publish_one, module, params) do
apply(module, :publish_one, [params])
end
def perform(:publish, activity) do
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
with %User{} = actor <- User.get_cached_by_ap_id(activity.data["actor"]),
{:ok, actor} <- User.ensure_keys_present(actor) do
Publisher.publish(actor, activity)
end
end
- def perform(:incoming_doc, doc) do
- Logger.info("Got document, trying to parse")
- OStatus.handle_incoming(doc)
- end
-
def perform(:incoming_ap_doc, params) do
Logger.info("Handling incoming AP activity")
params = Utils.normalize_params(params)
# NOTE: we use the actor ID to do the containment, this is fine because an
# actor shouldn't be acting on objects outside their own AP server.
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.normalize(params["id"]),
:ok <- Containment.contain_origin_from_id(params["actor"], params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, activity}
else
%Activity{} ->
Logger.info("Already had #{params["id"]}")
:error
_e ->
# Just drop those for now
Logger.info("Unhandled activity")
Logger.info(Jason.encode!(params, pretty: true))
:error
end
end
- def perform(:request_subscription, websub) do
- Logger.debug("Refreshing #{websub.topic}")
-
- with {:ok, websub} <- Websub.request_subscription(websub) do
- Logger.debug("Successfully refreshed #{websub.topic}")
- else
- _e -> Logger.debug("Couldn't refresh #{websub.topic}")
- end
- end
-
- def perform(:verify_websub, websub) do
- Logger.debug(fn ->
- "Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})"
- end)
-
- Websub.verify(websub)
- end
-
- def perform(:refresh_subscriptions) do
- Logger.debug("Federator running refresh subscriptions")
- Websub.refresh_subscriptions()
- end
-
def ap_enabled_actor(id) do
user = User.get_cached_by_ap_id(id)
if User.ap_enabled?(user) do
{:ok, user}
else
ActivityPub.make_user_from_ap_id(id)
end
end
end
diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex
index 937064638..fb9b26649 100644
--- a/lib/pleroma/web/federator/publisher.ex
+++ b/lib/pleroma/web/federator/publisher.ex
@@ -1,83 +1,109 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator.Publisher do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Workers.PublisherWorker
require Logger
@moduledoc """
Defines the contract used by federation implementations to publish messages to
their peers.
"""
@doc """
Determine whether an activity can be relayed using the federation module.
"""
@callback is_representable?(Pleroma.Activity.t()) :: boolean()
@doc """
Relays an activity to a specified peer, determined by the parameters. The
parameters used are controlled by the federation module.
"""
@callback publish_one(Map.t()) :: {:ok, Map.t()} | {:error, any()}
@doc """
Enqueue publishing a single activity.
"""
@spec enqueue_one(module(), Map.t()) :: :ok
def enqueue_one(module, %{} = params) do
PublisherWorker.enqueue(
"publish_one",
%{"module" => to_string(module), "params" => params}
)
end
@doc """
Relays an activity to all specified peers.
"""
@callback publish(User.t(), Activity.t()) :: :ok | {:error, any()}
@spec publish(User.t(), Activity.t()) :: :ok
def publish(%User{} = user, %Activity{} = activity) do
Config.get([:instance, :federation_publisher_modules])
|> Enum.each(fn module ->
if module.is_representable?(activity) do
Logger.info("Publishing #{activity.data["id"]} using #{inspect(module)}")
module.publish(user, activity)
end
end)
:ok
end
@doc """
Gathers links used by an outgoing federation module for WebFinger output.
"""
@callback gather_webfinger_links(User.t()) :: list()
@spec gather_webfinger_links(User.t()) :: list()
def gather_webfinger_links(%User{} = user) do
Config.get([:instance, :federation_publisher_modules])
|> Enum.reduce([], fn module, links ->
links ++ module.gather_webfinger_links(user)
end)
end
@doc """
Gathers nodeinfo protocol names supported by the federation module.
"""
@callback gather_nodeinfo_protocol_names() :: list()
@spec gather_nodeinfo_protocol_names() :: list()
def gather_nodeinfo_protocol_names do
Config.get([:instance, :federation_publisher_modules])
|> Enum.reduce([], fn module, links ->
links ++ module.gather_nodeinfo_protocol_names()
end)
end
+
+ @doc """
+ Gathers a set of remote users given an IR envelope.
+ """
+ def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
+ cc = Map.get(data, "cc", [])
+
+ bcc =
+ data
+ |> Map.get("bcc", [])
+ |> Enum.reduce([], fn ap_id, bcc ->
+ case Pleroma.List.get_by_ap_id(ap_id) do
+ %Pleroma.List{user_id: ^user_id} = list ->
+ {:ok, following} = Pleroma.List.get_following(list)
+ bcc ++ Enum.map(following, & &1.ap_id)
+
+ _ ->
+ bcc
+ end
+ end)
+
+ [to, cc, bcc]
+ |> Enum.concat()
+ |> Enum.map(&User.get_cached_by_ap_id/1)
+ |> Enum.filter(fn user -> user && !user.local end)
+ end
end
diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex
index 87860f1d5..ca261ad6e 100644
--- a/lib/pleroma/web/masto_fe_controller.ex
+++ b/lib/pleroma/web/masto_fe_controller.ex
@@ -1,48 +1,54 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastoFEController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings)
# Note: :index action handles attempt of unauthenticated access to private instance with redirect
plug(
OAuthScopesPlug,
%{scopes: ["read"], fallback: :proceed_unauthenticated, skip_instance_privacy_check: true}
when action == :index
)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index)
@doc "GET /web/*path"
def index(%{assigns: %{user: user}} = conn, _params) do
token = get_session(conn, :oauth_token)
if user && token do
conn
|> put_layout(false)
|> render("index.html", token: token, user: user, custom_emojis: Pleroma.Emoji.get_all())
else
conn
|> put_session(:return_to, conn.request_path)
|> redirect(to: "/web/login")
end
end
+ @doc "GET /web/manifest.json"
+ def manifest(conn, _params) do
+ conn
+ |> render("manifest.json")
+ end
+
@doc "PUT /api/web/settings"
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
- with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
+ with {:ok, _} <- User.mastodon_settings_update(user, settings) do
json(conn, %{})
else
e ->
conn
|> put_status(:internal_server_error)
|> json(%{error: inspect(e)})
end
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 9ef7fd48d..73fad519e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -1,393 +1,387 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]
alias Pleroma.Emoji
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action == :show
)
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
when action in [:endorsements, :verify_credentials, :followers, :following]
)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action == :blocks
)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
)
plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
# Note: :follows (POST /api/v1/follows) is the same as :follow, consider removing :follows
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} when action in [:follows, :follow, :unfollow]
)
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
plug(
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
when action != :create
)
@relations [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
plug(RateLimiter, :relations_actions when action in @relations)
plug(RateLimiter, :app_account_creation when action == :create)
plug(:assign_account_by_id when action in @needs_account)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@doc "POST /api/v1/accounts"
def create(
%{assigns: %{app: app}} = conn,
%{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
) do
params =
params
|> Map.take([
"email",
"captcha_solution",
"captcha_token",
"captcha_answer_data",
"token",
"password"
])
|> Map.put("nickname", nickname)
|> Map.put("fullname", params["fullname"] || nickname)
|> Map.put("bio", params["bio"] || "")
|> Map.put("confirm", params["password"])
with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
json(conn, %{
token_type: "Bearer",
access_token: token.token,
scope: app.scopes,
created_at: Token.Utils.format_created_at(token)
})
else
{:error, errors} -> json_response(conn, :bad_request, errors)
end
end
def create(%{assigns: %{app: _app}} = conn, _) do
render_error(conn, :bad_request, "Missing parameters")
end
def create(conn, _) do
render_error(conn, :forbidden, "Invalid credentials")
end
@doc "GET /api/v1/accounts/verify_credentials"
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
render(conn, "show.json",
user: user,
for: user,
with_pleroma_settings: true,
with_chat_token: chat_token
)
end
@doc "PATCH /api/v1/accounts/update_credentials"
def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
user = original_user
- user_params =
- %{}
- |> add_if_present(params, "display_name", :name)
- |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
- |> add_if_present(params, "avatar", :avatar, fn value ->
- with %Plug.Upload{} <- value,
- {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
- {:ok, object.data}
- end
- end)
-
- emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
-
- user_info_emojis =
- user.info
- |> Map.get(:emoji, [])
- |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
- |> Enum.dedup()
-
params =
if Map.has_key?(params, "fields_attributes") do
Map.update!(params, "fields_attributes", fn fields ->
fields
|> normalize_fields_attributes()
|> Enum.filter(fn %{"name" => n} -> n != "" end)
end)
else
params
end
- info_params =
+ user_params =
[
:no_rich_text,
:locked,
:hide_followers_count,
:hide_follows_count,
:hide_followers,
:hide_follows,
:hide_favorites,
:show_role,
:skip_thread_containment,
:discoverable
]
|> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
end)
- |> add_if_present(params, "default_scope", :default_scope)
- |> add_if_present(params, "fields_attributes", :fields, fn fields ->
- fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
-
- {:ok, fields}
- end)
- |> add_if_present(params, "fields_attributes", :raw_fields)
- |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
- {:ok, Map.merge(user.info.pleroma_settings_store, value)}
+ |> add_if_present(params, "display_name", :name)
+ |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
+ |> add_if_present(params, "avatar", :avatar, fn value ->
+ with %Plug.Upload{} <- value,
+ {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
+ {:ok, object.data}
+ end
end)
|> add_if_present(params, "header", :banner, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :banner) do
{:ok, object.data}
end
end)
|> add_if_present(params, "pleroma_background_image", :background, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :background) do
{:ok, object.data}
end
end)
- |> Map.put(:emoji, user_info_emojis)
+ |> add_if_present(params, "fields_attributes", :fields, fn fields ->
+ fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
- changeset =
+ {:ok, fields}
+ end)
+ |> add_if_present(params, "fields_attributes", :raw_fields)
+ |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
+ {:ok, Map.merge(user.pleroma_settings_store, value)}
+ end)
+ |> add_if_present(params, "default_scope", :default_scope)
+
+ emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
+
+ user_emojis =
user
- |> User.update_changeset(user_params)
- |> User.change_info(&User.Info.profile_update(&1, info_params))
+ |> Map.get(:emoji, [])
+ |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
+ |> Enum.dedup()
+
+ user_params = Map.put(user_params, :emoji, user_emojis)
+ changeset = User.update_changeset(user, user_params)
with {:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user, do: CommonAPI.update(user)
render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
else
_e -> render_error(conn, :forbidden, "Invalid request")
end
end
defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
with true <- Map.has_key?(params, params_field),
{:ok, new_value} <- value_function.(params[params_field]) do
Map.put(map, map_field, new_value)
else
_ -> map
end
end
defp normalize_fields_attributes(fields) do
if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end)
else
fields
end
end
@doc "GET /api/v1/accounts/relationships"
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
targets = User.get_all_by_ids(List.wrap(id))
render(conn, "relationships.json", user: user, targets: targets)
end
# Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id"
def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
render(conn, "show.json", user: user, for: for_user)
else
_e -> render_error(conn, :not_found, "Can't find user")
end
end
@doc "GET /api/v1/accounts/:id/statuses"
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
params = Map.put(params, "tag", params["tagged"])
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", activities: activities, for: reading_user, as: :activity)
end
end
@doc "GET /api/v1/accounts/:id/followers"
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
- user.info.hide_followers -> []
+ user.hide_followers -> []
true -> MastodonAPI.get_followers(user, params)
end
conn
|> add_link_headers(followers)
|> render("index.json", for: for_user, users: followers, as: :user)
end
@doc "GET /api/v1/accounts/:id/following"
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
- user.info.hide_follows -> []
+ user.hide_follows -> []
true -> MastodonAPI.get_friends(user, params)
end
conn
|> add_link_headers(followers)
|> render("index.json", for: for_user, users: followers, as: :user)
end
@doc "GET /api/v1/accounts/:id/lists"
def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
lists = Pleroma.List.get_lists_account_belongs(user, account)
conn
|> put_view(ListView)
|> render("index.json", lists: lists)
end
@doc "POST /api/v1/accounts/:id/follow"
def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, :not_found}
end
def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
render(conn, "relationship.json", user: follower, target: followed)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unfollow"
def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, :not_found}
end
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
render(conn, "relationship.json", user: follower, target: followed)
end
end
@doc "POST /api/v1/accounts/:id/mute"
def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
notifications? = params |> Map.get("notifications", true) |> truthy_param?()
with {:ok, muter} <- User.mute(muter, muted, notifications?) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unmute"
def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
with {:ok, muter} <- User.unmute(muter, muted) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/block"
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unblock"
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/follows"
def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
{_, true} <- {:followed, follower.id != followed.id},
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
render(conn, "show.json", user: followed, for: follower)
else
{:followed, _} -> {:error, :not_found}
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "GET /api/v1/mutes"
def mutes(%{assigns: %{user: user}} = conn, _) do
render(conn, "index.json", users: User.muted_users(user), for: user, as: :user)
end
@doc "GET /api/v1/blocks"
def blocks(%{assigns: %{user: user}} = conn, _) do
render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user)
end
@doc "GET /api/v1/endorsements"
def endorsements(conn, params),
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
index c7606246b..456fe7ab2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -1,39 +1,39 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
plug(
OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action == :index
)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:blocks"]} when action != :index
)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
@doc "GET /api/v1/domain_blocks"
- def index(%{assigns: %{user: %{info: info}}} = conn, _) do
- json(conn, Map.get(info, :domain_blocks, []))
+ def index(%{assigns: %{user: user}} = conn, _) do
+ json(conn, Map.get(user, :domain_blocks, []))
end
@doc "POST /api/v1/domain_blocks"
def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end
@doc "DELETE /api/v1/domain_blocks"
def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
new file mode 100644
index 000000000..ce025624d
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MarkerController do
+ use Pleroma.Web, :controller
+ alias Pleroma.Plugs.OAuthScopesPlug
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:statuses"]}
+ when action == :index
+ )
+
+ plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert)
+ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ # GET /api/v1/markers
+ def index(%{assigns: %{user: user}} = conn, params) do
+ markers = Pleroma.Marker.get_markers(user, params["timeline"])
+ render(conn, "markers.json", %{markers: markers})
+ end
+
+ # POST /api/v1/markers
+ def upsert(%{assigns: %{user: user}} = conn, params) do
+ with {:ok, result} <- Pleroma.Marker.upsert(user, params),
+ markers <- Map.values(result) do
+ render(conn, "markers.json", %{markers: markers})
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 2d4976891..e30fed610 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -1,264 +1,260 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountView do
use Pleroma.Web, :view
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MediaProxy
def render("index.json", %{users: users} = opts) do
users
|> render_many(AccountView, "show.json", opts)
|> Enum.filter(&Enum.any?/1)
end
def render("show.json", %{user: user} = opts) do
if User.visible_for?(user, opts[:for]),
do: do_render("show.json", opts),
else: %{}
end
def render("mention.json", %{user: user}) do
%{
id: to_string(user.id),
acct: user.nickname,
username: username_from_nickname(user.nickname),
url: User.profile_url(user)
}
end
def render("relationship.json", %{user: nil, target: _target}) do
%{}
end
def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do
follow_state = User.get_cached_follow_state(user, target)
requested =
if follow_state && !User.following?(user, target) do
follow_state == "pending"
else
false
end
%{
id: to_string(target.id),
following: User.following?(user, target),
followed_by: User.following?(target, user),
blocking: User.blocks_ap_id?(user, target),
blocked_by: User.blocks_ap_id?(target, user),
muting: User.mutes?(user, target),
muting_notifications: User.muted_notifications?(user, target),
subscribing: User.subscribed_to?(user, target),
requested: requested,
domain_blocking: User.blocks_domain?(user, target),
showing_reblogs: User.showing_reblogs?(user, target),
endorsed: false
}
end
def render("relationships.json", %{user: user, targets: targets}) do
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
end
defp do_render("show.json", %{user: user} = opts) do
display_name = HTML.strip_tags(user.name || user.nickname)
image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url()
user_info = User.get_cached_user_info(user)
following_count =
- if !user.info.hide_follows_count or !user.info.hide_follows or opts[:for] == user do
+ if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do
user_info.following_count
else
0
end
followers_count =
- if !user.info.hide_followers_count or !user.info.hide_followers or opts[:for] == user do
+ if !user.hide_followers_count or !user.hide_followers or opts[:for] == user do
user_info.follower_count
else
0
end
- bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]
+ bot = (user.source_data["type"] || "Person") in ["Application", "Service"]
emojis =
- (user.info.source_data["tag"] || [])
+ (user.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
%{
"shortcode" => String.trim(name, ":"),
"url" => MediaProxy.url(url),
"static_url" => MediaProxy.url(url),
"visible_in_picker" => false
}
end)
fields =
- user.info
- |> User.Info.fields()
+ user
+ |> User.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => Pleroma.HTML.strip_tags(name),
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
- raw_fields = Map.get(user.info, :raw_fields, [])
-
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
relationship = render("relationship.json", %{user: opts[:for], target: user})
- discoverable = user.info.discoverable
-
%{
id: to_string(user.id),
username: username_from_nickname(user.nickname),
acct: user.nickname,
display_name: display_name,
- locked: user_info.locked,
+ locked: user.locked,
created_at: Utils.to_masto_date(user.inserted_at),
followers_count: followers_count,
following_count: following_count,
- statuses_count: user_info.note_count,
+ statuses_count: user.note_count,
note: bio || "",
url: User.profile_url(user),
avatar: image,
avatar_static: image,
header: header,
header_static: header,
emojis: emojis,
fields: fields,
bot: bot,
source: %{
note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
sensitive: false,
- fields: raw_fields,
+ fields: user.raw_fields,
pleroma: %{
- discoverable: discoverable
+ discoverable: user.discoverable
}
},
# Pleroma extension
pleroma: %{
confirmation_pending: user_info.confirmation_pending,
tags: user.tags,
- hide_followers_count: user.info.hide_followers_count,
- hide_follows_count: user.info.hide_follows_count,
- hide_followers: user.info.hide_followers,
- hide_follows: user.info.hide_follows,
- hide_favorites: user.info.hide_favorites,
+ hide_followers_count: user.hide_followers_count,
+ hide_follows_count: user.hide_follows_count,
+ hide_followers: user.hide_followers,
+ hide_follows: user.hide_follows,
+ hide_favorites: user.hide_favorites,
relationship: relationship,
- skip_thread_containment: user.info.skip_thread_containment,
- background_image: image_url(user.info.background) |> MediaProxy.url()
+ skip_thread_containment: user.skip_thread_containment,
+ background_image: image_url(user.background) |> MediaProxy.url()
}
}
|> maybe_put_role(user, opts[:for])
|> maybe_put_settings(user, opts[:for], user_info)
|> maybe_put_notification_settings(user, opts[:for])
|> maybe_put_settings_store(user, opts[:for], opts)
|> maybe_put_chat_token(user, opts[:for], opts)
|> maybe_put_activation_status(user, opts[:for])
|> maybe_put_follow_requests_count(user, opts[:for])
|> maybe_put_unread_conversation_count(user, opts[:for])
end
defp username_from_nickname(string) when is_binary(string) do
hd(String.split(string, "@"))
end
defp username_from_nickname(_), do: nil
defp maybe_put_follow_requests_count(
data,
%User{id: user_id} = user,
%User{id: user_id}
) do
count =
User.get_follow_requests(user)
|> length()
data
|> Kernel.put_in([:follow_requests_count], count)
end
defp maybe_put_follow_requests_count(data, _, _), do: data
defp maybe_put_settings(
data,
%User{id: user_id} = user,
%User{id: user_id},
- user_info
+ _user_info
) do
data
- |> Kernel.put_in([:source, :privacy], user_info.default_scope)
- |> Kernel.put_in([:source, :pleroma, :show_role], user.info.show_role)
- |> Kernel.put_in([:source, :pleroma, :no_rich_text], user.info.no_rich_text)
+ |> Kernel.put_in([:source, :privacy], user.default_scope)
+ |> Kernel.put_in([:source, :pleroma, :show_role], user.show_role)
+ |> Kernel.put_in([:source, :pleroma, :no_rich_text], user.no_rich_text)
end
defp maybe_put_settings(data, _, _, _), do: data
- defp maybe_put_settings_store(data, %User{info: info, id: id}, %User{id: id}, %{
+ defp maybe_put_settings_store(data, %User{} = user, %User{}, %{
with_pleroma_settings: true
}) do
data
- |> Kernel.put_in([:pleroma, :settings_store], info.pleroma_settings_store)
+ |> Kernel.put_in([:pleroma, :settings_store], user.pleroma_settings_store)
end
defp maybe_put_settings_store(data, _, _, _), do: data
defp maybe_put_chat_token(data, %User{id: id}, %User{id: id}, %{
with_chat_token: token
}) do
data
|> Kernel.put_in([:pleroma, :chat_token], token)
end
defp maybe_put_chat_token(data, _, _, _), do: data
- defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do
+ defp maybe_put_role(data, %User{show_role: true} = user, _) do
data
- |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
- |> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator)
+ |> Kernel.put_in([:pleroma, :is_admin], user.is_admin)
+ |> Kernel.put_in([:pleroma, :is_moderator], user.is_moderator)
end
defp maybe_put_role(data, %User{id: user_id} = user, %User{id: user_id}) do
data
- |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
- |> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator)
+ |> Kernel.put_in([:pleroma, :is_admin], user.is_admin)
+ |> Kernel.put_in([:pleroma, :is_moderator], user.is_moderator)
end
defp maybe_put_role(data, _, _), do: data
defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
- Kernel.put_in(data, [:pleroma, :notification_settings], user.info.notification_settings)
+ Kernel.put_in(data, [:pleroma, :notification_settings], user.notification_settings)
end
defp maybe_put_notification_settings(data, _, _), do: data
- defp maybe_put_activation_status(data, user, %User{info: %{is_admin: true}}) do
- Kernel.put_in(data, [:pleroma, :deactivated], user.info.deactivated)
+ defp maybe_put_activation_status(data, user, %User{is_admin: true}) do
+ Kernel.put_in(data, [:pleroma, :deactivated], user.deactivated)
end
defp maybe_put_activation_status(data, _, _), do: data
defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{id: user_id}) do
data
|> Kernel.put_in(
[:pleroma, :unread_conversation_count],
- user.info.unread_conversation_count
+ user.unread_conversation_count
)
end
defp maybe_put_unread_conversation_count(data, _, _), do: data
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
end
diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex
new file mode 100644
index 000000000..38fbeed5f
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex
@@ -0,0 +1,17 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MarkerView do
+ use Pleroma.Web, :view
+
+ def render("markers.json", %{markers: markers}) do
+ Enum.reduce(markers, %{}, fn m, acc ->
+ Map.put_new(acc, m.timeline, %{
+ last_read_id: m.last_read_id,
+ version: m.lock_version,
+ updated_at: NaiveDateTime.to_iso8601(m.updated_at)
+ })
+ end)
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 9b8dd3086..b785ca9d4 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -1,503 +1,503 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusView do
use Pleroma.Web, :view
require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.PollView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]
# TODO: Add cached version.
defp get_replied_to_activities([]), do: %{}
defp get_replied_to_activities(activities) do
activities
|> Enum.map(fn
%{data: %{"type" => "Create"}} = activity ->
object = Object.normalize(activity)
object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
_ ->
nil
end)
|> Enum.filter(& &1)
|> Activity.create_by_object_ap_id_with_object()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
object = Object.normalize(activity)
if object, do: Map.put(acc, object.data["id"], activity), else: acc
end)
end
defp get_user(ap_id) do
cond do
user = User.get_cached_by_ap_id(ap_id) ->
user
user = User.get_by_guessed_nickname(ap_id) ->
user
true ->
User.error_user(ap_id)
end
end
defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
do: context_id
defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
do: Utils.context_to_conversation_id(context)
defp get_context_id(_), do: nil
defp reblogged?(activity, user) do
object = Object.normalize(activity) || %{}
present?(user && user.ap_id in (object.data["announcements"] || []))
end
def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities)
opts = Map.put(opts, :replied_to_activities, replied_to_activities)
safe_render_many(opts.activities, StatusView, "show.json", opts)
end
def render(
"show.json",
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
) do
user = get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
activity_object = Object.normalize(activity)
reblogged_activity =
Activity.create_by_object_ap_id(activity_object.data["id"])
|> Activity.with_preloaded_bookmark(opts[:for])
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.one()
reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
mentions =
activity.recipients
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
%{
id: to_string(activity.id),
uri: activity_object.data["id"],
url: activity_object.data["id"],
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
reblog: reblogged,
content: reblogged[:content] || "",
created_at: created_at,
reblogs_count: 0,
replies_count: 0,
favourites_count: 0,
reblogged: reblogged?(reblogged_activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: false,
pinned: pinned?(activity, user),
sensitive: false,
spoiler_text: "",
visibility: get_visibility(activity),
media_attachments: reblogged[:media_attachments] || [],
mentions: mentions,
tags: reblogged[:tags] || [],
application: %{
name: "Web",
website: nil
},
language: nil,
emojis: [],
pleroma: %{
local: activity.local
}
}
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity)
user = get_user(activity.data["actor"])
user_follower_address = user.follower_address
like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0
tags = object.data["tag"] || []
sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
tag_mentions =
tags
|> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
|> Enum.map(fn tag -> tag["href"] end)
mentions =
(object.data["to"] ++ tag_mentions)
|> Enum.uniq()
|> Enum.map(fn
Pleroma.Constants.as_public() -> nil
^user_follower_address -> nil
ap_id -> User.get_cached_by_ap_id(ap_id)
end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
client_posted_this_activity = opts[:for] && user.id == opts[:for].id
expires_at =
with true <- client_posted_this_activity,
expiration when not is_nil(expiration) <-
ActivityExpiration.get_by_activity_id(activity.id) do
expiration.scheduled_at
end
thread_muted? =
case activity.thread_muted? do
thread_muted? when is_boolean(thread_muted?) -> thread_muted?
nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
end
attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object.data["published"])
reply_to = get_reply_to(activity, opts)
reply_to_user = reply_to && get_user(reply_to.data["actor"])
content =
object
|> render_content()
content_html =
content
|> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"mastoapi:content"
)
content_plaintext =
content
|> HTML.get_cached_stripped_html_for_activity(
activity,
"mastoapi:content"
)
summary = object.data["summary"] || ""
summary_html =
summary
|> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"mastoapi:summary"
)
summary_plaintext =
summary
|> HTML.get_cached_stripped_html_for_activity(
activity,
"mastoapi:summary"
)
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
url =
if user.local do
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
else
object.data["url"] || object.data["external_url"] || object.data["id"]
end
direct_conversation_id =
with {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
{_, %User{} = for_user} <- {:for_user, opts[:for]},
%{data: %{"context" => context}} when is_binary(context) <- activity,
%Conversation{} = conversation <- Conversation.get_for_ap_id(context),
%Participation{id: participation_id} <-
Participation.for_user_and_conversation(for_user, conversation) do
participation_id
else
_e ->
nil
end
%{
id: to_string(activity.id),
uri: object.data["id"],
url: url,
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
card: card,
content: content_html,
created_at: created_at,
reblogs_count: announcement_count,
replies_count: object.data["repliesCount"] || 0,
favourites_count: like_count,
reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: thread_muted? || User.mutes?(opts[:for], user),
pinned: pinned?(activity, user),
sensitive: sensitive,
spoiler_text: summary_html,
visibility: get_visibility(object),
media_attachments: attachments,
poll: render(PollView, "show.json", object: object, for: opts[:for]),
mentions: mentions,
tags: build_tags(tags),
application: %{
name: "Web",
website: nil
},
language: nil,
emojis: build_emojis(object.data["emoji"]),
pleroma: %{
local: activity.local,
conversation_id: get_context_id(activity),
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary_plaintext},
expires_at: expires_at,
direct_conversation_id: direct_conversation_id,
thread_muted: thread_muted?
}
}
end
def render("show.json", _) do
nil
end
def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
page_url_data = URI.parse(page_url)
page_url_data =
if rich_media[:url] != nil do
URI.merge(page_url_data, URI.parse(rich_media[:url]))
else
page_url_data
end
page_url = page_url_data |> to_string
image_url =
if rich_media[:image] != nil do
URI.merge(page_url_data, URI.parse(rich_media[:image]))
|> to_string
else
nil
end
site_name = rich_media[:site_name] || page_url_data.host
%{
type: "link",
provider_name: site_name,
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url,
image: image_url |> MediaProxy.url(),
title: rich_media[:title] || "",
description: rich_media[:description] || "",
pleroma: %{
opengraph: rich_media
}
}
end
def render("card.json", _), do: nil
def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
href = attachment_url["href"] |> MediaProxy.url()
type =
cond do
String.contains?(media_type, "image") -> "image"
String.contains?(media_type, "video") -> "video"
String.contains?(media_type, "audio") -> "audio"
true -> "unknown"
end
<<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
%{
id: to_string(attachment["id"] || hash_id),
url: href,
remote_url: href,
preview_url: href,
text_url: href,
type: type,
description: attachment["name"],
pleroma: %{mime_type: media_type}
}
end
def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
object = Object.normalize(activity)
user = get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
%{
id: activity.id,
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
created_at: created_at,
title: object.data["title"] |> HTML.strip_tags(),
artist: object.data["artist"] |> HTML.strip_tags(),
album: object.data["album"] |> HTML.strip_tags(),
length: object.data["length"]
}
end
def render("listens.json", opts) do
safe_render_many(opts.activities, StatusView, "listen.json", opts)
end
def render("context.json", %{activity: activity, activities: activities, user: user}) do
%{ancestors: ancestors, descendants: descendants} =
activities
|> Enum.reverse()
|> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
|> Map.put_new(:ancestors, [])
|> Map.put_new(:descendants, [])
%{
ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
descendants: render("index.json", for: user, activities: descendants, as: :activity)
}
end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity)
with nil <- replied_to_activities[object.data["inReplyTo"]] do
# If user didn't participate in the thread
Activity.get_in_reply_to_activity(activity)
end
end
def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
object = Object.normalize(activity)
if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
else
nil
end
end
def render_content(%{data: %{"type" => "Video"}} = object) do
with name when not is_nil(name) and name != "" <- object.data["name"] do
"<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
else
_ -> object.data["content"] || ""
end
end
def render_content(%{data: %{"type" => object_type}} = object)
when object_type in ["Article", "Page"] do
with summary when not is_nil(summary) and summary != "" <- object.data["name"],
url when is_bitstring(url) <- object.data["url"] do
"<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
else
_ -> object.data["content"] || ""
end
end
def render_content(object), do: object.data["content"] || ""
@doc """
Builds a dictionary tags.
## Examples
iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
[{"name": "fediverse", "url": "/tag/fediverse"},
{"name": "nextcloud", "url": "/tag/nextcloud"}]
"""
@spec build_tags(list(any())) :: list(map())
def build_tags(object_tags) when is_list(object_tags) do
object_tags = for tag when is_binary(tag) <- object_tags, do: tag
Enum.reduce(object_tags, [], fn tag, tags ->
tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
end)
end
def build_tags(_), do: []
@doc """
Builds list emojis.
Arguments: `nil` or list tuple of name and url.
Returns list emojis.
## Examples
iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
[%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
"""
@spec build_emojis(nil | list(tuple())) :: list(map())
def build_emojis(nil), do: []
def build_emojis(emojis) do
emojis
|> Enum.map(fn {name, url} ->
name = HTML.strip_tags(name)
url =
url
|> HTML.strip_tags()
|> MediaProxy.url()
%{shortcode: name, url: url, static_url: url, visible_in_picker: false}
end)
end
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
- defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}),
+ defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
do: id in pinned_activities
end
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 3c26eb406..a400d1c8d 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -1,136 +1,143 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
require Logger
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Streamer
@behaviour :cowboy_websocket
@streams [
"public",
"public:local",
"public:media",
"public:local:media",
"user",
"user:notification",
"direct",
"list",
"hashtag"
]
@anonymous_streams ["public", "public:local", "hashtag"]
# Handled by periodic keepalive in Pleroma.Web.Streamer.Ping.
@timeout :infinity
def init(%{qs: qs} = req, state) do
with params <- :cow_qs.parse_qs(qs),
sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
access_token <- List.keyfind(params, "access_token", 0),
{_, stream} <- List.keyfind(params, "stream", 0),
{:ok, user} <- allow_request(stream, [access_token, sec_websocket]),
topic when is_binary(topic) <- expand_topic(stream, params) do
+ req =
+ if sec_websocket do
+ :cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req)
+ else
+ req
+ end
+
{:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
else
{:error, code} ->
Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
{:ok, req} = :cowboy_req.reply(code, req)
{:ok, req, state}
error ->
Logger.debug("#{__MODULE__} denied connection: #{inspect(error)} - #{inspect(req)}")
{:ok, req} = :cowboy_req.reply(400, req)
{:ok, req, state}
end
end
def websocket_init(state) do
send(self(), :subscribe)
{:ok, state}
end
# We never receive messages.
def websocket_handle(_frame, state) do
{:ok, state}
end
def websocket_info(:subscribe, state) do
Logger.debug(
"#{__MODULE__} accepted websocket connection for user #{
(state.user || %{id: "anonymous"}).id
}, topic #{state.topic}"
)
Streamer.add_socket(state.topic, streamer_socket(state))
{:ok, state}
end
def websocket_info({:text, message}, state) do
{:reply, {:text, message}, state}
end
def terminate(reason, _req, state) do
Logger.debug(
"#{__MODULE__} terminating websocket connection for user #{
(state.user || %{id: "anonymous"}).id
}, topic #{state.topic || "?"}: #{inspect(reason)}"
)
Streamer.remove_socket(state.topic, streamer_socket(state))
:ok
end
# Public streams without authentication.
defp allow_request(stream, [nil, nil]) when stream in @anonymous_streams do
{:ok, nil}
end
# Authenticated streams.
defp allow_request(stream, [access_token, sec_websocket]) when stream in @streams do
token =
with {"access_token", token} <- access_token do
token
else
_ -> sec_websocket
end
with true <- is_bitstring(token),
%Token{user_id: user_id} <- Repo.get_by(Token, token: token),
user = %User{} <- User.get_cached_by_id(user_id) do
{:ok, user}
else
_ -> {:error, 403}
end
end
# Not authenticated.
defp allow_request(stream, _) when stream in @streams, do: {:error, 403}
# No matching stream.
defp allow_request(_, _), do: {:error, 404}
defp expand_topic("hashtag", params) do
case List.keyfind(params, "tag", 0) do
{_, tag} -> "hashtag:#{tag}"
_ -> nil
end
end
defp expand_topic("list", params) do
case List.keyfind(params, "list", 0) do
{_, list} -> "list:#{list}"
_ -> nil
end
end
defp expand_topic(topic, _), do: topic
defp streamer_socket(state) do
%{transport_pid: self(), assigns: state}
end
end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 03c9a5027..fe71aca8c 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -1,490 +1,490 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Pleroma.Helpers.UriHelper
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Auth.Authenticator
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
alias Pleroma.Web.OAuth.Scopes
require Logger
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
plug(:fetch_session)
plug(:fetch_flash)
plug(Pleroma.Plugs.RateLimiter, :authentication when action == :create_authorization)
action_fallback(Pleroma.Web.OAuth.FallbackController)
@oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
# Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
{auth_attrs, params} = Map.pop(params, "authorization")
authorize(conn, Map.merge(params, auth_attrs))
end
def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do
if ControllerHelper.truthy_param?(params["force_login"]) do
do_authorize(conn, params)
else
handle_existing_authorization(conn, params)
end
end
def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
defp do_authorize(%Plug.Conn{} = conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes)
# Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
render(conn, Authenticator.auth_template(), %{
response_type: params["response_type"],
client_id: params["client_id"],
available_scopes: available_scopes,
scopes: scopes,
redirect_uri: params["redirect_uri"],
state: params["state"],
params: params
})
end
defp handle_existing_authorization(
%Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
%{"redirect_uri" => @oob_token_redirect_uri}
) do
render(conn, "oob_token_exists.html", %{token: token})
end
defp handle_existing_authorization(
%Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
%{} = params
) do
app = Repo.preload(token, :app).app
redirect_uri =
if is_binary(params["redirect_uri"]) do
params["redirect_uri"]
else
default_redirect_uri(app)
end
if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{access_token: token.token}
url_params = UriHelper.append_param_if_present(url_params, :state, params["state"])
url = UriHelper.append_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
conn
|> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
|> redirect(external: redirect_uri(conn, redirect_uri))
end
end
def create_authorization(
%Plug.Conn{} = conn,
%{"authorization" => _} = params,
opts \\ []
) do
with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do
after_create_authorization(conn, auth, params)
else
error ->
handle_create_authorization_error(conn, error, params)
end
end
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
"authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
}) do
render(conn, "oob_authorization_created.html", %{auth: auth})
end
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
"authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
}) do
app = Repo.preload(auth, :app).app
# An extra safety measure before we redirect (also done in `do_create_authorization/2`)
if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{code: auth.token}
url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"])
url = UriHelper.append_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
conn
|> put_flash(:error, dgettext("errors", "Unlisted redirect_uri."))
|> redirect(external: redirect_uri(conn, redirect_uri))
end
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:error, scopes_issue},
%{"authorization" => _} = params
)
when scopes_issue in [:unsupported_scopes, :missing_scopes] do
# Per https://github.com/tootsuite/mastodon/blob/
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
conn
|> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
|> put_status(:unauthorized)
|> authorize(params)
end
defp handle_create_authorization_error(
%Plug.Conn{} = conn,
{:auth_active, false},
%{"authorization" => _} = params
) do
# Per https://github.com/tootsuite/mastodon/blob/
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
conn
|> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address"))
|> put_status(:forbidden)
|> authorize(params)
end
defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
Authenticator.handle_error(conn, error)
end
@doc "Renew access_token with refresh_token"
def token_exchange(
%Plug.Conn{} = conn,
%{"grant_type" => "refresh_token", "refresh_token" => token} = _params
) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)}
json(conn, Token.Response.build(user, token, response_attrs))
else
_error -> render_invalid_credentials_error(conn)
end
end
def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
fixed_token = Token.Utils.fix_padding(params["code"]),
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)}
json(conn, Token.Response.build(user, token, response_attrs))
else
_error -> render_invalid_credentials_error(conn)
end
end
def token_exchange(
%Plug.Conn{} = conn,
%{"grant_type" => "password"} = params
) do
with {:ok, %User{} = user} <- Authenticator.get_user(conn),
{:ok, app} <- Token.Utils.fetch_app(conn),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
- {:user_active, true} <- {:user_active, !user.info.deactivated},
+ {:user_active, true} <- {:user_active, !user.deactivated},
{:password_reset_pending, false} <-
- {:password_reset_pending, user.info.password_reset_pending},
+ {:password_reset_pending, user.password_reset_pending},
{:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build(user, token))
else
{:auth_active, false} ->
# Per https://github.com/tootsuite/mastodon/blob/
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
render_error(
conn,
:forbidden,
"Your login is missing a confirmed e-mail address",
%{},
"missing_confirmed_email"
)
{:user_active, false} ->
render_error(
conn,
:forbidden,
"Your account is currently disabled",
%{},
"account_is_disabled"
)
{:password_reset_pending, true} ->
render_error(
conn,
:forbidden,
"Password reset is required",
%{},
"password_reset_required"
)
_error ->
render_invalid_credentials_error(conn)
end
end
def token_exchange(
%Plug.Conn{} = conn,
%{"grant_type" => "password", "name" => name, "password" => _password} = params
) do
params =
params
|> Map.delete("name")
|> Map.put("username", name)
token_exchange(conn, params)
end
def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build_for_client_credentials(token))
else
_error -> render_invalid_credentials_error(conn)
end
end
# Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, _token} <- RevokeToken.revoke(app, params) do
json(conn, %{})
else
_error ->
# RFC 7009: invalid tokens [in the request] do not cause an error response
json(conn, %{})
end
end
def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
# Response for bad request
defp bad_request(%Plug.Conn{} = conn, _) do
render_error(conn, :internal_server_error, "Bad request")
end
@doc "Prepares OAuth request to provider for Ueberauth"
def prepare_request(%Plug.Conn{} = conn, %{
"provider" => provider,
"authorization" => auth_attrs
}) do
scope =
auth_attrs
|> Scopes.fetch_scopes([])
|> Scopes.to_string()
state =
auth_attrs
|> Map.delete("scopes")
|> Map.put("scope", scope)
|> Jason.encode!()
params =
auth_attrs
|> Map.drop(~w(scope scopes client_id redirect_uri))
|> Map.put("state", state)
# Handing the request to Ueberauth
redirect(conn, to: o_auth_path(conn, :request, provider, params))
end
def request(%Plug.Conn{} = conn, params) do
message =
if params["provider"] do
dgettext("errors", "Unsupported OAuth provider: %{provider}.",
provider: params["provider"]
)
else
dgettext("errors", "Bad OAuth request.")
end
conn
|> put_flash(:error, message)
|> redirect(to: "/")
end
def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
params = callback_params(params)
messages = for e <- Map.get(failure, :errors, []), do: e.message
message = Enum.join(messages, "; ")
conn
|> put_flash(
:error,
dgettext("errors", "Failed to authenticate: %{message}.", message: message)
)
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
end
def callback(%Plug.Conn{} = conn, params) do
params = callback_params(params)
with {:ok, registration} <- Authenticator.get_registration(conn) do
auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state))
case Repo.get_assoc(registration, :user) do
{:ok, user} ->
create_authorization(conn, %{"authorization" => auth_attrs}, user: user)
_ ->
registration_params =
Map.merge(auth_attrs, %{
"nickname" => Registration.nickname(registration),
"email" => Registration.email(registration)
})
conn
|> put_session_registration_id(registration.id)
|> registration_details(%{"authorization" => registration_params})
end
else
error ->
Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
conn
|> put_flash(:error, dgettext("errors", "Failed to set up user account."))
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
end
end
defp callback_params(%{"state" => state} = params) do
Map.merge(params, Jason.decode!(state))
end
def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
render(conn, "register.html", %{
client_id: auth_attrs["client_id"],
redirect_uri: auth_attrs["redirect_uri"],
state: auth_attrs["state"],
scopes: Scopes.fetch_scopes(auth_attrs, []),
nickname: auth_attrs["nickname"],
email: auth_attrs["email"]
})
end
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
{_, {:ok, auth}} <- {:create_authorization, do_create_authorization(conn, params)},
%User{} = user <- Repo.preload(auth, :user).user,
{:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
conn
|> put_session_registration_id(nil)
|> after_create_authorization(auth, params)
else
{:create_authorization, error} ->
{:register, handle_create_authorization_error(conn, error, params)}
_ ->
{:register, :generic_error}
end
end
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
{:ok, user} <- Authenticator.create_from_registration(conn, registration) do
conn
|> put_session_registration_id(nil)
|> create_authorization(
params,
user: user
)
else
{:error, changeset} ->
message =
Enum.map(changeset.errors, fn {field, {error, _}} ->
"#{field} #{error}"
end)
|> Enum.join("; ")
message =
String.replace(
message,
"ap_id has already been taken",
"nickname has already been taken"
)
conn
|> put_status(:forbidden)
|> put_flash(:error, "Error: #{message}.")
|> registration_details(params)
_ ->
{:register, :generic_error}
end
end
defp do_create_authorization(
%Plug.Conn{} = conn,
%{
"authorization" =>
%{
"client_id" => client_id,
"redirect_uri" => redirect_uri
} = auth_attrs
},
user \\ nil
) do
with {_, {:ok, %User{} = user}} <-
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
%App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris),
{:ok, scopes} <- validate_scopes(app, auth_attrs),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)} do
Authorization.create_authorization(app, user, scopes)
end
end
# Special case: Local MastodonFE
defp redirect_uri(%Plug.Conn{} = conn, "."), do: auth_url(conn, :login)
defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
do: put_session(conn, :registration_id, registration_id)
@spec validate_scopes(App.t(), map()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
defp validate_scopes(app, params) do
params
|> Scopes.fetch_scopes(app.scopes)
|> Scopes.validate(app.scopes)
end
def default_redirect_uri(%App{} = app) do
app.redirect_uris
|> String.split()
|> Enum.at(0)
end
defp render_invalid_credentials_error(conn) do
render_error(conn, :bad_request, "Invalid credentials")
end
end
diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex
deleted file mode 100644
index 8e55b9f0b..000000000
--- a/lib/pleroma/web/ostatus/activity_representer.ex
+++ /dev/null
@@ -1,313 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/web/ostatus/feed_representer.ex b/lib/pleroma/web/ostatus/feed_representer.ex
deleted file mode 100644
index b7b97e505..000000000
--- a/lib/pleroma/web/ostatus/feed_representer.ex
+++ /dev/null
@@ -1,66 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/web/ostatus/handlers/delete_handler.ex b/lib/pleroma/web/ostatus/handlers/delete_handler.ex
deleted file mode 100644
index ac2dc115c..000000000
--- a/lib/pleroma/web/ostatus/handlers/delete_handler.ex
+++ /dev/null
@@ -1,18 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OStatus.DeleteHandler do
- require Logger
- alias Pleroma.Object
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.XML
-
- def handle_delete(entry, _doc \\ nil) do
- with id <- XML.string_from_xpath("//id", entry),
- %Object{} = object <- Object.normalize(id),
- {:ok, delete} <- ActivityPub.delete(object, local: false) do
- delete
- end
- end
-end
diff --git a/lib/pleroma/web/ostatus/handlers/follow_handler.ex b/lib/pleroma/web/ostatus/handlers/follow_handler.ex
deleted file mode 100644
index 24513972e..000000000
--- a/lib/pleroma/web/ostatus/handlers/follow_handler.ex
+++ /dev/null
@@ -1,26 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex
deleted file mode 100644
index 7fae14f7b..000000000
--- a/lib/pleroma/web/ostatus/handlers/note_handler.ex
+++ /dev/null
@@ -1,168 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex
deleted file mode 100644
index 2062432e3..000000000
--- a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex
+++ /dev/null
@@ -1,22 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
deleted file mode 100644
index 5de1ceef3..000000000
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ /dev/null
@@ -1,395 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 20f2d9ddc..6958519de 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -1,208 +1,164 @@
# 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.OStatusController do
use Pleroma.Web, :controller
alias Fallback.RedirectController
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
- alias Pleroma.Web.Federator
alias Pleroma.Web.Metadata.PlayerView
- alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.Router
- alias Pleroma.Web.XML
plug(
Pleroma.Plugs.RateLimiter,
{:ap_routes, params: ["uuid"]} when action in [:object, :activity]
)
- plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
-
plug(
Pleroma.Plugs.SetFormatPlug
when action in [:object, :activity, :notice]
)
action_fallback(:errors)
- defp decode_or_retry(body) do
- with {:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
- {:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
- {:ok, doc}
- else
- _e ->
- with [decoded | _] <- Pleroma.Web.Salmon.decode(body),
- doc <- XML.parse_document(decoded),
- uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
- {:ok, _} <- Pleroma.Web.OStatus.make_user(uri, true),
- {:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
- {:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
- {:ok, doc}
- end
- end
- end
-
- def salmon_incoming(conn, _) do
- {:ok, body, _conn} = read_body(conn)
- {:ok, doc} = decode_or_retry(body)
-
- Federator.incoming_doc(doc)
-
- send_resp(conn, 200, "")
- end
-
def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :object)
end
def object(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
with id <- o_status_url(conn, :object, uuid),
{_, %Activity{} = activity} <-
{:activity, Activity.get_create_by_object_ap_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format do
"html" -> redirect(conn, to: "/notice/#{activity.id}")
_ -> represent_activity(conn, nil, activity, user)
end
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found}
e ->
e
end
end
def activity(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :activity)
end
def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
with id <- o_status_url(conn, :activity, uuid),
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format do
"html" -> redirect(conn, to: "/notice/#{activity.id}")
_ -> represent_activity(conn, format, activity, user)
end
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found}
e ->
e
end
end
def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
cond do
format == "html" && activity.data["type"] == "Create" ->
%Object{} = object = Object.normalize(activity)
RedirectController.redirector_with_meta(
conn,
%{
activity_id: activity.id,
object: object,
url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id),
user: user
}
)
format == "html" ->
RedirectController.redirector(conn, nil)
true ->
represent_activity(conn, format, activity, user)
end
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
conn
|> put_status(404)
|> RedirectController.redirector(nil, 404)
e ->
e
end
end
# Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
def notice_player(conn, %{"id" => id}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.is_public?(activity),
%Object{} = object <- Object.normalize(activity),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
conn
|> put_layout(:metadata_player)
|> put_resp_header("x-frame-options", "ALLOW")
|> put_resp_header(
"content-security-policy",
"default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;"
)
|> put_view(PlayerView)
|> render("player.html", url)
else
_error ->
conn
|> put_status(404)
|> RedirectController.redirector(nil, 404)
end
end
defp represent_activity(
conn,
"activity+json",
%Activity{data: %{"type" => "Create"}} = activity,
_user
) do
object = Object.normalize(activity)
conn
|> put_resp_header("content-type", "application/activity+json")
|> put_view(ObjectView)
|> render("object.json", %{object: object})
end
- defp represent_activity(_conn, "activity+json", _, _) do
+ defp represent_activity(_conn, _, _, _) do
{:error, :not_found}
end
- defp represent_activity(conn, _, activity, user) do
- response =
- activity
- |> ActivityRepresenter.to_simple_form(user, true)
- |> ActivityRepresenter.wrap_with_entry()
- |> :xmerl.export_simple(:xmerl_xml)
- |> to_string
-
- conn
- |> put_resp_content_type("application/atom+xml")
- |> send_resp(200, response)
- end
-
def errors(conn, {:error, :not_found}) do
render_error(conn, :not_found, "Not found")
end
def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
def errors(conn, _) do
render_error(conn, :internal_server_error, "Something went wrong")
end
end
diff --git a/lib/pleroma/web/ostatus/user_representer.ex b/lib/pleroma/web/ostatus/user_representer.ex
deleted file mode 100644
index 852be6eb4..000000000
--- a/lib/pleroma/web/ostatus/user_representer.ex
+++ /dev/null
@@ -1,41 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index 9012e2175..ee40bbf33 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -1,168 +1,162 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.AccountController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
alias Ecto.Changeset
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
require Pleroma.Constants
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"]}
# Note: the following actions are not permission-secured in Mastodon:
when action in [
:update_avatar,
:update_banner,
:update_background
]
)
plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
# An extra safety measure for possible actions not guarded by OAuth permissions specification
plug(
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
when action != :confirmation_resend
)
plug(RateLimiter, :account_confirmation_resend when action == :confirmation_resend)
plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
@doc "POST /api/v1/pleroma/accounts/confirmation_resend"
def confirmation_resend(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
{:ok, _} <- User.try_send_confirmation_email(user) do
json_response(conn, :no_content, "")
end
end
@doc "PATCH /api/v1/pleroma/accounts/update_avatar"
def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
{:ok, user} =
user
|> Changeset.change(%{avatar: nil})
|> User.update_and_set_cache()
CommonAPI.update(user)
json(conn, %{url: nil})
end
def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
{:ok, user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
%{"url" => [%{"href" => href} | _]} = data
CommonAPI.update(user)
json(conn, %{url: href})
end
@doc "PATCH /api/v1/pleroma/accounts/update_banner"
def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
- new_info = %{"banner" => %{}}
-
- with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
+ with {:ok, user} <- User.update_banner(user, %{}) do
CommonAPI.update(user)
json(conn, %{url: nil})
end
end
def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
- new_info <- %{"banner" => object.data},
- {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
+ {:ok, user} <- User.update_banner(user, object.data) do
CommonAPI.update(user)
%{"url" => [%{"href" => href} | _]} = object.data
json(conn, %{url: href})
end
end
@doc "PATCH /api/v1/pleroma/accounts/update_background"
def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
- new_info = %{"background" => %{}}
-
- with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
+ with {:ok, _user} <- User.update_background(user, %{}) do
json(conn, %{url: nil})
end
end
def update_background(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(params, type: :background),
- new_info <- %{"background" => object.data},
- {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
+ {:ok, _user} <- User.update_background(user, object.data) do
%{"url" => [%{"href" => href} | _]} = object.data
json(conn, %{url: href})
end
end
@doc "GET /api/v1/pleroma/accounts/:id/favourites"
- def favourites(%{assigns: %{account: %{info: %{hide_favorites: true}}}} = conn, _params) do
+ def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do
render_error(conn, :forbidden, "Can't get favorites")
end
def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", for_user)
recipients =
if for_user do
[Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
else
[Pleroma.Constants.as_public()]
end
activities =
recipients
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", activities: activities, for: for_user, as: :activity)
end
@doc "POST /api/v1/pleroma/accounts/:id/subscribe"
def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
with {:ok, subscription_target} <- User.subscribe(user, subscription_target) do
render(conn, "relationship.json", user: user, target: subscription_target)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/pleroma/accounts/:id/unsubscribe"
def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
with {:ok, subscription_target} <- User.unsubscribe(user, subscription_target) do
render(conn, "relationship.json", user: user, target: subscription_target)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
index d71d72dd5..8cf552b7e 100644
--- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -1,41 +1,39 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.MascotController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
@doc "GET /api/v1/pleroma/mascot"
def show(%{assigns: %{user: user}} = conn, _params) do
json(conn, User.get_mascot(user))
end
@doc "PUT /api/v1/pleroma/mascot"
def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do
with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
# Reject if not an image
%{type: "image"} = attachment <- render_attachment(object) do
- # Sure!
- # Save to the user's info
- {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, attachment))
+ {:ok, _user} = User.mascot_update(user, attachment)
json(conn, attachment)
else
%{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
end
end
defp render_attachment(object) do
attachment_data = Map.put(object.data, "id", object.id)
Pleroma.Web.MastodonAPI.StatusView.render("attachment.json", %{attachment: attachment_data})
end
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
index 9d50a7ca9..651a99423 100644
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
@@ -1,104 +1,113 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.MastodonAPI.ConversationView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"]} when action in [:conversation, :conversation_statuses]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:conversations"]} when action == :update_conversation
)
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
with %Participation{} = participation <- Participation.get(participation_id),
true <- user.id == participation.user_id do
conn
|> put_view(ConversationView)
|> render("participation.json", %{participation: participation, for: user})
end
end
def conversation_statuses(
%{assigns: %{user: user}} = conn,
%{"id" => participation_id} = params
) do
participation = Participation.get(participation_id, preload: [:conversation])
if user.id == participation.user_id do
params =
params
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
activities =
participation.conversation.ap_id
|> ActivityPub.fetch_activities_for_context(params)
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
end
def update_conversation(
%{assigns: %{user: user}} = conn,
%{"id" => participation_id, "recipients" => recipients}
) do
participation =
participation_id
|> Participation.get()
with true <- user.id == participation.user_id,
{:ok, participation} <- Participation.set_recipients(participation, recipients) do
conn
|> put_view(ConversationView)
|> render("participation.json", %{participation: participation, for: user})
end
end
+ def read_conversations(%{assigns: %{user: user}} = conn, _params) do
+ with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
+ conn
+ |> add_link_headers(participations)
+ |> put_view(ConversationView)
+ |> render("participations.json", participations: participations, for: user)
+ end
+ end
+
def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
with {:ok, notification} <- Notification.read_one(user, notification_id) do
conn
|> put_view(NotificationView)
|> render("show.json", %{notification: notification, for: user})
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{"error" => message})
end
end
def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do
with notifications <- Notification.set_read_up_to(user, max_id) do
notifications = Enum.take(notifications, 80)
conn
|> put_view(NotificationView)
|> render("index.json", %{notifications: notifications, for: user})
end
end
end
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index 35d3ff07c..dd445e8bf 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -1,136 +1,140 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.Impl do
@moduledoc "The module represents implementation push web notification"
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Metadata.Utils
alias Pleroma.Web.Push.Subscription
require Logger
import Ecto.Query
@types ["Create", "Follow", "Announce", "Like"]
@doc "Performs sending notifications for user subscriptions"
@spec perform(Notification.t()) :: list(any) | :error
def perform(
%{
activity: %{data: %{"type" => activity_type}, id: activity_id} = activity,
user_id: user_id
} = notif
)
when activity_type in @types do
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
type = Activity.mastodon_notification_type(notif.activity)
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
object = Object.normalize(activity)
for subscription <- fetch_subsriptions(user_id),
get_in(subscription.data, ["alerts", type]) do
%{
title: format_title(notif),
access_token: subscription.token.token,
body: format_body(notif, actor, object),
notification_id: notif.id,
notification_type: type,
icon: avatar_url,
preferred_locale: "en",
pleroma: %{
activity_id: activity_id
}
}
|> Jason.encode!()
|> push_message(build_sub(subscription), gcm_api_key, subscription)
end
end
def perform(_) do
Logger.warn("Unknown notification type")
:error
end
@doc "Push message to web"
def push_message(body, sub, api_key, subscription) do
case WebPushEncryption.send_web_push(body, sub, api_key) do
{:ok, %{status_code: code}} when 400 <= code and code < 500 ->
Logger.debug("Removing subscription record")
Repo.delete!(subscription)
:ok
{:ok, %{status_code: code}} when 200 <= code and code < 300 ->
:ok
{:ok, %{status_code: code}} ->
Logger.error("Web Push Notification failed with code: #{code}")
:error
_ ->
Logger.error("Web Push Notification failed with unknown error")
:error
end
end
@doc "Gets user subscriptions"
def fetch_subsriptions(user_id) do
Subscription
|> where(user_id: ^user_id)
|> preload(:token)
|> Repo.all()
end
def build_sub(subscription) do
%{
keys: %{
p256dh: subscription.key_p256dh,
auth: subscription.key_auth
},
endpoint: subscription.endpoint
}
end
def format_body(
%{activity: %{data: %{"type" => "Create"}}},
actor,
%{data: %{"content" => content}}
) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
%{activity: %{data: %{"type" => "Announce"}}},
actor,
%{data: %{"content" => content}}
) do
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
%{activity: %{data: %{"type" => type}}},
actor,
_object
)
when type in ["Follow", "Like"] do
case type do
"Follow" -> "@#{actor.nickname} has followed you"
"Like" -> "@#{actor.nickname} has favorited your post"
end
end
+ def format_title(%{activity: %{data: %{"directMessage" => true}}}) do
+ "New Direct Message"
+ end
+
def format_title(%{activity: %{data: %{"type" => type}}}) do
case type do
"Create" -> "New Mention"
"Follow" -> "New Follower"
"Announce" -> "New Repeat"
"Like" -> "New Favorite"
end
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index ae799b8ac..f69c5c2bc 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,631 +1,646 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
end
pipeline :oauth do
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
end
pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
end
pipeline :authenticated_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
end
pipeline :admin_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.AdminSecretAuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.UserIsAdminPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
end
pipeline :mastodon_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
pipeline :pleroma_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
pipeline :well_known do
plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
end
pipeline :config do
plug(:accepts, ["json", "xml"])
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
end
pipeline :mailbox_preview do
plug(:accepts, ["html"])
plug(:put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' 'unsafe-eval'"
})
end
pipeline :http_signature do
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api)
get("/password_reset/:token", PasswordController, :reset, as: :reset_password)
post("/password_reset", PasswordController, :do_reset, as: :reset_password)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
end
scope "/api/pleroma", Pleroma.Web do
pipe_through(:pleroma_api)
post("/uploader_callback/:upload_path", UploaderController, :callback)
end
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
post("/users/follow", AdminAPIController, :user_follow)
post("/users/unfollow", AdminAPIController, :user_unfollow)
delete("/users", AdminAPIController, :user_delete)
post("/users", AdminAPIController, :users_create)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
+ patch("/users/activate", AdminAPIController, :user_activate)
+ patch("/users/deactivate", AdminAPIController, :user_deactivate)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
+
post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
delete(
"/users/:nickname/permission_group/:permission_group",
AdminAPIController,
:right_delete
)
- put("/users/:nickname/activation_status", AdminAPIController, :set_activation_status)
+ post("/users/permission_group/:permission_group", AdminAPIController, :right_add_multiple)
+
+ delete(
+ "/users/permission_group/:permission_group",
+ AdminAPIController,
+ :right_delete_multiple
+ )
+ get("/relay", AdminAPIController, :relay_list)
post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)
post("/users/invite_token", AdminAPIController, :create_invite_token)
get("/users/invites", AdminAPIController, :invites)
post("/users/revoke_invite", AdminAPIController, :revoke_invite)
post("/users/email_invite", AdminAPIController, :email_invite)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/:nickname/force_password_reset", AdminAPIController, :force_password_reset)
get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/reports", AdminAPIController, :list_reports)
get("/reports/:id", AdminAPIController, :report_show)
put("/reports/:id", AdminAPIController, :report_update_state)
post("/reports/:id/respond", AdminAPIController, :report_respond)
put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete)
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
get("/config/migrate_to_db", AdminAPIController, :migrate_to_db)
get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/packs" do
# Modifying packs
pipe_through(:admin_api)
post("/import_from_fs", EmojiAPIController, :import_from_fs)
post("/:pack_name/update_file", EmojiAPIController, :update_file)
post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)
put("/:name", EmojiAPIController, :create)
delete("/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from)
post("/list_from", EmojiAPIController, :list_from)
end
scope "/packs" do
# Pack info / downloading
get("/", EmojiAPIController, :list_packs)
get("/:name/download_shared/", EmojiAPIController, :download_shared)
end
end
scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
post("/main/ostatus", UtilController, :remote_subscribe)
get("/ostatus_subscribe", UtilController, :remote_follow)
post("/ostatus_subscribe", UtilController, :do_remote_follow)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:authenticated_api)
post("/change_email", UtilController, :change_email)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings)
post("/disable_account", UtilController, :disable_account)
post("/blocks_import", UtilController, :blocks_import)
post("/follow_import", UtilController, :follow_import)
end
scope "/oauth", Pleroma.Web.OAuth do
scope [] do
pipe_through(:oauth)
get("/authorize", OAuthController, :authorize)
end
post("/authorize", OAuthController, :create_authorization)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
get("/registration_details", OAuthController, :registration_details)
scope [] do
pipe_through(:browser)
get("/prepare_request", OAuthController, :prepare_request)
get("/:provider", OAuthController, :request)
get("/:provider/callback", OAuthController, :callback)
post("/register", OAuthController, :register)
end
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
+ post("/conversations/read", PleromaAPIController, :read_conversations)
end
scope [] do
pipe_through(:authenticated_api)
patch("/conversations/:id", PleromaAPIController, :update_conversation)
post("/notifications/read", PleromaAPIController, :read_notification)
patch("/accounts/update_avatar", AccountController, :update_avatar)
patch("/accounts/update_banner", AccountController, :update_banner)
patch("/accounts/update_background", AccountController, :update_background)
get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)
post("/scrobble", ScrobbleController, :new_scrobble)
end
scope [] do
pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites)
end
scope [] do
pipe_through(:authenticated_api)
post("/accounts/:id/subscribe", AccountController, :subscribe)
post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
end
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
get("/accounts/verify_credentials", AccountController, :verify_credentials)
get("/accounts/relationships", AccountController, :relationships)
get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
get("/follow_requests", FollowRequestController, :index)
get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/favourites", StatusController, :favourites)
get("/bookmarks", StatusController, :bookmarks)
get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show)
post("/notifications/clear", NotificationController, :clear)
post("/notifications/dismiss", NotificationController, :dismiss)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show)
get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)
get("/domain_blocks", DomainBlockController, :index)
get("/filters", FilterController, :index)
get("/suggestions", SuggestionController, :index)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :read)
get("/endorsements", AccountController, :endorsements)
patch("/accounts/update_credentials", AccountController, :update_credentials)
post("/statuses", StatusController, :create)
delete("/statuses/:id", StatusController, :delete)
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite)
post("/statuses/:id/unfavourite", StatusController, :unfavourite)
post("/statuses/:id/pin", StatusController, :pin)
post("/statuses/:id/unpin", StatusController, :unpin)
post("/statuses/:id/bookmark", StatusController, :bookmark)
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
post("/polls/:id/votes", PollController, :vote)
post("/media", MediaController, :create)
put("/media/:id", MediaController, :update)
delete("/lists/:id", ListController, :delete)
post("/lists", ListController, :create)
put("/lists/:id", ListController, :update)
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)
post("/filters", FilterController, :create)
get("/filters/:id", FilterController, :show)
put("/filters/:id", FilterController, :update)
delete("/filters/:id", FilterController, :delete)
post("/reports", ReportController, :create)
post("/follows", AccountController, :follows)
post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block)
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
post("/follow_requests/:id/reject", FollowRequestController, :reject)
post("/domain_blocks", DomainBlockController, :create)
delete("/domain_blocks", DomainBlockController, :delete)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :get)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
+
+ get("/markers", MarkerController, :index)
+ post("/markers", MarkerController, :upsert)
end
scope "/api/web", Pleroma.Web do
pipe_through(:authenticated_api)
put("/settings", MastoFEController, :put_settings)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
post("/accounts", AccountController, :create)
get("/accounts/search", SearchController, :account_search)
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
post("/apps", AppController, :create)
get("/apps/verify_credentials", AppController, :verify_credentials)
get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/custom_emojis", CustomEmojiController, :index)
get("/trends", MastodonAPIController, :empty_array)
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
get("/timelines/list/:list_id", TimelineController, :list)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
get("/polls/:id", PollController, :show)
get("/accounts/:id/statuses", AccountController, :statuses)
get("/accounts/:id/followers", AccountController, :followers)
get("/accounts/:id/following", AccountController, :following)
get("/accounts/:id", AccountController, :show)
get("/search", SearchController, :search)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/search", SearchController, :search2)
end
scope "/api", Pleroma.Web do
pipe_through(:config)
get("/help/test", TwitterAPI.UtilController, :help_test)
post("/help/test", TwitterAPI.UtilController, :help_test)
get("/statusnet/config", TwitterAPI.UtilController, :config)
get("/statusnet/version", TwitterAPI.UtilController, :version)
get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
end
scope "/api", Pleroma.Web do
pipe_through(:api)
get(
"/account/confirm_email/:user_id/:token",
TwitterAPI.Controller,
:confirm_email,
as: :confirm_email
)
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)
get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
end
pipeline :ap_service_actor do
plug(:accepts, ["activity+json", "json"])
end
pipeline :ostatus do
plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
end
pipeline :oembed do
plug(:accepts, ["json", "xml"])
end
scope "/", Pleroma.Web do
pipe_through(:ostatus)
pipe_through(:http_signature)
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
get("/users/:nickname/feed", Feed.FeedController, :feed)
get("/users/:nickname", Feed.FeedController, :feed_redirect)
- post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
- post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
- get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
- post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
-
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end
pipeline :activitypub do
plug(:accepts, ["activity+json", "json"])
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end
scope "/", Pleroma.Web.ActivityPub do
# XXX: not really ostatus
pipe_through(:ostatus)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
pipeline :activitypub_client do
plug(:accepts, ["activity+json", "json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_client])
get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
end
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :relay)
scope [] do
pipe_through(:http_signature)
post("/inbox", ActivityPubController, :inbox)
end
get("/following", ActivityPubController, :following, assigns: %{relay: true})
get("/followers", ActivityPubController, :followers, assigns: %{relay: true})
end
scope "/internal/fetch", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :internal_fetch)
post("/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end
scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
+ scope "/", Pleroma.Web do
+ pipe_through(:api)
+
+ get("/web/manifest.json", MastoFEController, :manifest)
+ end
+
scope "/", Pleroma.Web do
pipe_through(:mastodon_html)
get("/web/login", MastodonAPI.AuthController, :login)
delete("/auth/sign_out", MastodonAPI.AuthController, :logout)
post("/auth/password", MastodonAPI.AuthController, :password_reset)
get("/web/*path", MastoFEController, :index)
end
pipeline :remote_media do
end
scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
if Pleroma.Config.get(:env) == :dev do
scope "/dev" do
pipe_through([:mailbox_preview])
forward("/mailbox", Plug.Swoosh.MailboxPreview, base_path: "/dev/mailbox")
end
end
scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password)
end
scope "/", Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
get("/api*path", RedirectController, :api_not_implemented)
get("/*path", RedirectController, :redirector)
options("/*path", RedirectController, :empty)
end
end
diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex
deleted file mode 100644
index 0ffe903cd..000000000
--- a/lib/pleroma/web/salmon/salmon.ex
+++ /dev/null
@@ -1,254 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex
index 8cf719277..2fc7ac8cf 100644
--- a/lib/pleroma/web/streamer/streamer.ex
+++ b/lib/pleroma/web/streamer/streamer.ex
@@ -1,55 +1,55 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer do
alias Pleroma.Web.Streamer.State
alias Pleroma.Web.Streamer.Worker
@timeout 60_000
@mix_env Mix.env()
def add_socket(topic, socket) do
State.add_socket(topic, socket)
end
def remove_socket(topic, socket) do
State.remove_socket(topic, socket)
end
def get_sockets do
State.get_sockets()
end
def stream(topics, items) do
if should_send?() do
Task.async(fn ->
:poolboy.transaction(
:streamer_worker,
&Worker.stream(&1, topics, items),
@timeout
)
end)
end
end
def supervisor, do: Pleroma.Web.Streamer.Supervisor
defp should_send? do
handle_should_send(@mix_env)
end
defp handle_should_send(:test) do
case Process.whereis(:streamer_worker) do
nil ->
false
pid ->
Process.alive?(pid)
end
end
- defp handle_should_send(_) do
- true
- end
+ defp handle_should_send(:benchmark), do: false
+
+ defp handle_should_send(_), do: true
end
diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex
index 0ea224874..c2ee9e1f5 100644
--- a/lib/pleroma/web/streamer/worker.ex
+++ b/lib/pleroma/web/streamer/worker.ex
@@ -1,224 +1,224 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer.Worker do
use GenServer
require Logger
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Streamer.State
alias Pleroma.Web.Streamer.StreamerSocket
alias Pleroma.Web.StreamerView
def start_link(_) do
GenServer.start_link(__MODULE__, %{}, [])
end
def init(init_arg) do
{:ok, init_arg}
end
def stream(pid, topics, items) do
GenServer.call(pid, {:stream, topics, items})
end
def handle_call({:stream, topics, item}, _from, state) when is_list(topics) do
Enum.each(topics, fn t ->
do_stream(%{topic: t, item: item})
end)
{:reply, state, state}
end
def handle_call({:stream, topic, items}, _from, state) when is_list(items) do
Enum.each(items, fn i ->
do_stream(%{topic: topic, item: i})
end)
{:reply, state, state}
end
def handle_call({:stream, topic, item}, _from, state) do
do_stream(%{topic: topic, item: item})
{:reply, state, state}
end
defp do_stream(%{topic: "direct", item: item}) do
recipient_topics =
User.get_recipients_from_activity(item)
|> Enum.map(fn %{id: id} -> "direct:#{id}" end)
Enum.each(recipient_topics, fn user_topic ->
Logger.debug("Trying to push direct message to #{user_topic}\n\n")
push_to_socket(State.get_sockets(), user_topic, item)
end)
end
defp do_stream(%{topic: "participation", item: participation}) do
user_topic = "direct:#{participation.user_id}"
Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n")
push_to_socket(State.get_sockets(), user_topic, participation)
end
defp do_stream(%{topic: "list", item: item}) do
# filter the recipient list if the activity is not public, see #270.
recipient_lists =
case Visibility.is_public?(item) do
true ->
Pleroma.List.get_lists_from_activity(item)
_ ->
Pleroma.List.get_lists_from_activity(item)
|> Enum.filter(fn list ->
owner = User.get_cached_by_id(list.user_id)
Visibility.visible_for_user?(item, owner)
end)
end
recipient_topics =
recipient_lists
|> Enum.map(fn %{id: id} -> "list:#{id}" end)
Enum.each(recipient_topics, fn list_topic ->
Logger.debug("Trying to push message to #{list_topic}\n\n")
push_to_socket(State.get_sockets(), list_topic, item)
end)
end
defp do_stream(%{topic: topic, item: %Notification{} = item})
when topic in ["user", "user:notification"] do
State.get_sockets()
|> Map.get("#{topic}:#{item.user_id}", [])
|> Enum.each(fn %StreamerSocket{transport_pid: transport_pid, user: socket_user} ->
with %User{} = user <- User.get_cached_by_ap_id(socket_user.ap_id),
true <- should_send?(user, item) do
send(transport_pid, {:text, StreamerView.render("notification.json", socket_user, item)})
end
end)
end
defp do_stream(%{topic: "user", item: item}) do
Logger.debug("Trying to push to users")
recipient_topics =
User.get_recipients_from_activity(item)
|> Enum.map(fn %{id: id} -> "user:#{id}" end)
Enum.each(recipient_topics, fn topic ->
push_to_socket(State.get_sockets(), topic, item)
end)
end
defp do_stream(%{topic: topic, item: item}) do
Logger.debug("Trying to push to #{topic}")
Logger.debug("Pushing item to #{topic}")
push_to_socket(State.get_sockets(), topic, item)
end
defp should_send?(%User{} = user, %Activity{} = item) do
- blocks = user.info.blocks || []
- mutes = user.info.mutes || []
- reblog_mutes = user.info.muted_reblogs || []
+ blocks = user.blocks || []
+ mutes = user.mutes || []
+ reblog_mutes = user.muted_reblogs || []
recipient_blocks = MapSet.new(blocks ++ mutes)
recipients = MapSet.new(item.recipients)
- domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
+ domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
with parent when not is_nil(parent) <- Object.normalize(item),
true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)),
true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)),
true <- MapSet.disjoint?(recipients, recipient_blocks),
%{host: item_host} <- URI.parse(item.actor),
%{host: parent_host} <- URI.parse(parent.data["actor"]),
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
true <- thread_containment(item, user),
false <- CommonAPI.thread_muted?(user, item) do
true
else
_ -> false
end
end
defp should_send?(%User{} = user, %Notification{activity: activity}) do
should_send?(user, activity)
end
def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do
Enum.each(topics[topic] || [], fn %StreamerSocket{
transport_pid: transport_pid,
user: socket_user
} ->
# Get the current user so we have up-to-date blocks etc.
if socket_user do
user = User.get_cached_by_ap_id(socket_user.ap_id)
if should_send?(user, item) do
send(transport_pid, {:text, StreamerView.render("update.json", item, user)})
end
else
send(transport_pid, {:text, StreamerView.render("update.json", item)})
end
end)
end
def push_to_socket(topics, topic, %Participation{} = participation) do
Enum.each(topics[topic] || [], fn %StreamerSocket{transport_pid: transport_pid} ->
send(transport_pid, {:text, StreamerView.render("conversation.json", participation)})
end)
end
def push_to_socket(topics, topic, %Activity{
data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id}
}) do
Enum.each(topics[topic] || [], fn %StreamerSocket{transport_pid: transport_pid} ->
send(
transport_pid,
{:text, %{event: "delete", payload: to_string(deleted_activity_id)} |> Jason.encode!()}
)
end)
end
def push_to_socket(_topics, _topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
def push_to_socket(topics, topic, item) do
Enum.each(topics[topic] || [], fn %StreamerSocket{
transport_pid: transport_pid,
user: socket_user
} ->
# Get the current user so we have up-to-date blocks etc.
if socket_user do
user = User.get_cached_by_ap_id(socket_user.ap_id)
if should_send?(user, item) do
send(transport_pid, {:text, StreamerView.render("update.json", item, user)})
end
else
send(transport_pid, {:text, StreamerView.render("update.json", item)})
end
end)
end
@spec thread_containment(Activity.t(), User.t()) :: boolean()
- defp thread_containment(_activity, %User{info: %{skip_thread_containment: true}}), do: true
+ defp thread_containment(_activity, %User{skip_thread_containment: true}), do: true
defp thread_containment(activity, user) do
if Config.get([:instance, :skip_thread_containment]) do
true
else
ActivityPub.contain_activity(activity, user)
end
end
end
diff --git a/lib/pleroma/web/templates/feed/feed/feed.xml.eex b/lib/pleroma/web/templates/feed/feed/feed.xml.eex
index fbfdc46b5..45df9dc09 100644
--- a/lib/pleroma/web/templates/feed/feed/feed.xml.eex
+++ b/lib/pleroma/web/templates/feed/feed/feed.xml.eex
@@ -1,26 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<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><%= feed_url(@conn, :feed, @user.nickname) <> ".atom" %></id>
<title><%= @user.nickname <> "'s timeline" %></title>
<updated><%= most_recent_update(@activities, @user) %></updated>
<logo><%= logo(@user) %></logo>
- <link rel="hub" href="<%= websub_url(@conn, :websub_subscription_request, @user.nickname) %>"/>
- <link rel="salmon" href="<%= o_status_url(@conn, :salmon_incoming, @user.nickname) %>"/>
<link rel="self" href="<%= '#{feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/>
<%= render @view_module, "_author.xml", assigns %>
<%= if last_activity(@activities) do %>
<link rel="next" href="<%= '#{feed_url(@conn, :feed, @user.nickname)}.atom?max_id=#{last_activity(@activities).id}' %>" type="application/atom+xml"/>
<% end %>
<%= for activity <- @activities do %>
<%= render @view_module, "_activity.xml", Map.merge(assigns, %{activity: activity, data: activity_object_data(activity)}) %>
<% end %>
</feed>
diff --git a/lib/pleroma/web/templates/masto_fe/index.html.eex b/lib/pleroma/web/templates/masto_fe/index.html.eex
index feff36fae..c330960fa 100644
--- a/lib/pleroma/web/templates/masto_fe/index.html.eex
+++ b/lib/pleroma/web/templates/masto_fe/index.html.eex
@@ -1,31 +1,35 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta content='width=device-width, initial-scale=1' name='viewport'>
<title>
-<%= Pleroma.Config.get([:instance, :name]) %>
+<%= Config.get([:instance, :name]) %>
</title>
<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/glitch/en.js"></script>
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/getting_started.js'>
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/compose.js'>
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/home_timeline.js'>
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/notifications.js'>
<script id='initial-state' type='application/json'><%= initial_state(@token, @user, @custom_emojis) %></script>
<script src="/packs/core/common.js"></script>
<link rel="stylesheet" media="all" href="/packs/core/common.css" />
<script src="/packs/flavours/glitch/common.js"></script>
<link rel="stylesheet" media="all" href="/packs/flavours/glitch/common.css" />
<script src="/packs/flavours/glitch/home.js"></script>
</head>
<body class='app-body no-reduce-motion system-font'>
<div class='app-holder' data-props='{&quot;locale&quot;:&quot;en&quot;}' id='mastodon'>
</div>
</body>
</html>
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index bf5a6ae42..39f10c49f 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -1,87 +1,88 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.Controller do
use Pleroma.Web, :controller
alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TokenView
require Logger
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(:errors)
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
- new_info = [need_confirmation: false]
-
- with %User{info: info} = user <- User.get_cached_by_id(uid),
- true <- user.local and info.confirmation_pending and info.confirmation_token == token,
- {:ok, _} <- User.update_info(user, &User.Info.confirmation_changeset(&1, new_info)) do
+ with %User{} = user <- User.get_cached_by_id(uid),
+ true <- user.local and user.confirmation_pending and user.confirmation_token == token,
+ {:ok, _} <-
+ user
+ |> User.confirmation_changeset(need_confirmation: false)
+ |> User.update_and_set_cache() do
redirect(conn, to: "/")
end
end
def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
with oauth_tokens <- Token.get_user_tokens(user) do
conn
|> put_view(TokenView)
|> render("index.json", %{tokens: oauth_tokens})
end
end
def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
Token.delete_user_token(user, id)
json_reply(conn, 201, "")
end
def errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
end
def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
defp json_reply(conn, status, json) do
conn
|> put_resp_content_type("application/json")
|> send_resp(status, json)
end
def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
Notification.set_read_up_to(user, latest_id)
notifications = Notification.for_user(user, params)
conn
# XXX: This is a hack because pleroma-fe still uses that API.
|> put_view(Pleroma.Web.MastodonAPI.NotificationView)
|> render("index.json", %{notifications: notifications, for: user})
end
def notifications_read(%{assigns: %{user: _user}} = conn, _) do
bad_request_reply(conn, "You need to specify latest_id")
end
defp bad_request_reply(conn, error_message) do
json = error_json(conn, error_message)
json_reply(conn, 400, json)
end
defp error_json(conn, error_message) do
%{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
end
end
diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex
index 21b086d4c..c39b7f095 100644
--- a/lib/pleroma/web/views/masto_fe_view.ex
+++ b/lib/pleroma/web/views/masto_fe_view.ex
@@ -1,102 +1,121 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastoFEView do
use Pleroma.Web, :view
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.CustomEmojiView
@default_settings %{
onboarded: true,
home: %{
shows: %{
reblog: true,
reply: true
}
},
notifications: %{
alerts: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
shows: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
sounds: %{
follow: true,
favourite: true,
reblog: true,
mention: true
}
}
}
def initial_state(token, user, custom_emojis) do
limit = Config.get([:instance, :limit])
%{
meta: %{
streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
access_token: token,
locale: "en",
domain: Pleroma.Web.Endpoint.host(),
admin: "1",
me: "#{user.id}",
unfollow_modal: false,
boost_modal: false,
delete_modal: true,
auto_play_gif: false,
display_sensitive_media: false,
reduce_motion: false,
max_toot_chars: limit,
mascot: User.get_mascot(user)["url"]
},
poll_limits: Config.get([:instance, :poll_limits]),
rights: %{
- delete_others_notice: present?(user.info.is_moderator),
- admin: present?(user.info.is_admin)
+ delete_others_notice: present?(user.is_moderator),
+ admin: present?(user.is_admin)
},
compose: %{
me: "#{user.id}",
- default_privacy: user.info.default_scope,
+ default_privacy: user.default_scope,
default_sensitive: false,
allow_content_types: Config.get([:instance, :allowed_post_formats])
},
media_attachments: %{
accept_content_types: [
".jpg",
".jpeg",
".png",
".gif",
".webm",
".mp4",
".m4v",
"image\/jpeg",
"image\/png",
"image\/gif",
"video\/webm",
"video\/mp4"
]
},
- settings: user.info.settings || @default_settings,
+ settings: user.settings || @default_settings,
push_subscription: nil,
accounts: %{user.id => render(AccountView, "show.json", user: user, for: user)},
custom_emojis: render(CustomEmojiView, "index.json", custom_emojis: custom_emojis),
char_limit: limit
}
|> Jason.encode!()
|> Phoenix.HTML.raw()
end
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
+
+ def render("manifest.json", _params) do
+ %{
+ name: Config.get([:instance, :name]),
+ description: Config.get([:instance, :description]),
+ icons: Config.get([:manifest, :icons]),
+ theme_color: Config.get([:manifest, :theme_color]),
+ background_color: Config.get([:manifest, :background_color]),
+ display: "standalone",
+ scope: Pleroma.Web.base_url(),
+ start_url: masto_fe_path(Pleroma.Web.Endpoint, :index, ["getting-started"]),
+ categories: [
+ "social"
+ ],
+ serviceworker: %{
+ src: "/sw.js"
+ }
+ }
+ end
end
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index ecb39ee50..b4cc80179 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -1,239 +1,227 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger do
alias Pleroma.HTTP
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.XML
alias Pleroma.XmlBuilder
require Jason
require Logger
def host_meta do
base_url = Web.base_url()
{
:XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
{
:Link,
%{
rel: "lrdd",
type: "application/xrd+xml",
template: "#{base_url}/.well-known/webfinger?resource={uri}"
}
}
}
|> XmlBuilder.to_doc()
end
def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
host = Pleroma.Web.Endpoint.host()
regex = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/
with %{"username" => username} <- Regex.named_captures(regex, resource),
%User{} = user <- User.get_cached_by_nickname(username) do
{:ok, represent_user(user, fmt)}
else
_e ->
with %User{} = user <- User.get_cached_by_ap_id(resource) do
{:ok, represent_user(user, fmt)}
else
_e ->
{:error, "Couldn't find user"}
end
end
end
defp gather_links(%User{} = user) do
[
%{
"rel" => "http://webfinger.net/rel/profile-page",
"type" => "text/html",
"href" => user.ap_id
}
] ++ Publisher.gather_webfinger_links(user)
end
def represent_user(user, "JSON") do
{:ok, user} = User.ensure_keys_present(user)
%{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
"aliases" => [user.ap_id],
"links" => gather_links(user)
}
end
def represent_user(user, "XML") do
{:ok, user} = User.ensure_keys_present(user)
links =
gather_links(user)
|> Enum.map(fn link -> {:Link, link} end)
{
:XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"},
{:Alias, user.ap_id}
] ++ links
}
|> XmlBuilder.to_doc()
end
defp get_magic_key("data:application/magic-public-key," <> magic_key) do
{:ok, magic_key}
end
defp get_magic_key(nil) do
Logger.debug("Undefined magic key.")
{:ok, nil}
end
defp get_magic_key(_) do
{:error, "Missing magic key data."}
end
defp webfinger_from_xml(doc) do
with magic_key <- XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc),
{:ok, magic_key} <- get_magic_key(magic_key),
topic <-
XML.string_from_xpath(
~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href},
doc
),
subject <- XML.string_from_xpath("//Subject", doc),
- salmon <- XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc),
subscribe_address <-
XML.string_from_xpath(
~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
doc
),
ap_id <-
XML.string_from_xpath(
~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
doc
) do
data = %{
"magic_key" => magic_key,
"topic" => topic,
"subject" => subject,
- "salmon" => salmon,
"subscribe_address" => subscribe_address,
"ap_id" => ap_id
}
{:ok, data}
else
{:error, e} ->
{:error, e}
e ->
{:error, e}
end
end
defp webfinger_from_json(doc) do
data =
Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
case {link["type"], link["rel"]} do
{"application/activity+json", "self"} ->
Map.put(data, "ap_id", link["href"])
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
Map.put(data, "ap_id", link["href"])
- {_, "magic-public-key"} ->
- "data:application/magic-public-key," <> magic_key = link["href"]
- Map.put(data, "magic_key", magic_key)
-
- {"application/atom+xml", "http://schemas.google.com/g/2010#updates-from"} ->
- Map.put(data, "topic", link["href"])
-
- {_, "salmon"} ->
- Map.put(data, "salmon", link["href"])
-
{_, "http://ostatus.org/schema/1.0/subscribe"} ->
Map.put(data, "subscribe_address", link["template"])
_ ->
Logger.debug("Unhandled type: #{inspect(link["type"])}")
data
end
end)
{:ok, data}
end
def get_template_from_xml(body) do
xpath = "//Link[@rel='lrdd']/@template"
with doc when doc != :error <- XML.parse_document(body),
template when template != nil <- XML.string_from_xpath(xpath, doc) do
{:ok, template}
end
end
def find_lrdd_template(domain) do
with {:ok, %{status: status, body: body}} when status in 200..299 <-
HTTP.get("http://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body)
else
_ ->
with {:ok, %{body: body}} <- HTTP.get("https://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body)
else
e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
end
end
end
@spec finger(String.t()) :: {:ok, map()} | {:error, any()}
def finger(account) do
account = String.trim_leading(account, "@")
domain =
with [_name, domain] <- String.split(account, "@") do
domain
else
_e ->
URI.parse(account).host
end
address =
case find_lrdd_template(domain) do
{:ok, template} ->
String.replace(template, "{uri}", URI.encode(account))
_ ->
"https://#{domain}/.well-known/webfinger?resource=acct:#{account}"
end
with response <-
HTTP.get(
address,
Accept: "application/xrd+xml,application/jrd+json"
),
{:ok, %{status: status, body: body}} when status in 200..299 <- response do
doc = XML.parse_document(body)
if doc != :error do
webfinger_from_xml(doc)
else
with {:ok, doc} <- Jason.decode(body) do
webfinger_from_json(doc)
end
end
else
e ->
Logger.debug(fn -> "Couldn't finger #{account}" end)
Logger.debug(fn -> inspect(e) end)
{:error, e}
end
end
end
diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex
deleted file mode 100644
index b61f388b8..000000000
--- a/lib/pleroma/web/websub/websub.ex
+++ /dev/null
@@ -1,332 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/web/websub/websub_client_subscription.ex b/lib/pleroma/web/websub/websub_client_subscription.ex
deleted file mode 100644
index 23a04b87d..000000000
--- a/lib/pleroma/web/websub/websub_client_subscription.ex
+++ /dev/null
@@ -1,20 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/web/websub/websub_controller.ex b/lib/pleroma/web/websub/websub_controller.ex
deleted file mode 100644
index 9e8b48b80..000000000
--- a/lib/pleroma/web/websub/websub_controller.ex
+++ /dev/null
@@ -1,99 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/web/websub/websub_server_subscription.ex b/lib/pleroma/web/websub/websub_server_subscription.ex
deleted file mode 100644
index d0ef548da..000000000
--- a/lib/pleroma/web/websub/websub_server_subscription.ex
+++ /dev/null
@@ -1,17 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex
index 83d528a66..8ad756b62 100644
--- a/lib/pleroma/workers/receiver_worker.ex
+++ b/lib/pleroma/workers/receiver_worker.ex
@@ -1,18 +1,14 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.ReceiverWorker do
alias Pleroma.Web.Federator
use Pleroma.Workers.WorkerHelper, queue: "federator_incoming"
@impl Oban.Worker
- def perform(%{"op" => "incoming_doc", "body" => doc}, _job) do
- Federator.perform(:incoming_doc, doc)
- end
-
def perform(%{"op" => "incoming_ap_doc", "params" => params}, _job) do
Federator.perform(:incoming_ap_doc, params)
end
end
diff --git a/lib/pleroma/workers/subscriber_worker.ex b/lib/pleroma/workers/subscriber_worker.ex
deleted file mode 100644
index fc490e300..000000000
--- a/lib/pleroma/workers/subscriber_worker.ex
+++ /dev/null
@@ -1,26 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <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
diff --git a/mix.exs b/mix.exs
index 6cf766c52..dcb9d9ea8 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,277 +1,278 @@
defmodule Pleroma.Mixfile do
use Mix.Project
def project do
[
app: :pleroma,
- version: version("1.0.0"),
+ version: version("1.1.50"),
elixir: "~> 1.8",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
elixirc_options: [warnings_as_errors: true],
xref: [exclude: [:eldap]],
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
test_coverage: [tool: ExCoveralls],
preferred_cli_env: ["coveralls.html": :test],
# Docs
name: "Pleroma",
homepage_url: "https://pleroma.social/",
source_url: "https://git.pleroma.social/pleroma/pleroma",
docs: [
source_url_pattern:
"https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}",
logo: "priv/static/static/logo.png",
extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/**/*.md"),
groups_for_extras: [
"Installation manuals": Path.wildcard("docs/installation/*.md"),
Configuration: Path.wildcard("docs/config/*.md"),
Administration: Path.wildcard("docs/admin/*.md"),
"Pleroma's APIs and Mastodon API extensions": Path.wildcard("docs/api/*.md")
],
main: "readme",
output: "priv/static/doc"
],
releases: [
pleroma: [
include_executables_for: [:unix],
applications: [ex_syslogger: :load, syslog: :load],
steps: [:assemble, &copy_files/1, &copy_nginx_config/1]
]
]
]
end
def copy_files(%{path: target_path} = release) do
File.cp_r!("./rel/files", target_path)
release
end
def copy_nginx_config(%{path: target_path} = release) do
File.cp!(
"./installation/pleroma.nginx",
Path.join([target_path, "installation", "pleroma.nginx"])
)
release
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {Pleroma.Application, []},
extra_applications: [:logger, :runtime_tools, :comeonin, :quack],
included_applications: [:ex_syslogger]
]
end
# Specifies which paths to compile per environment.
+ defp elixirc_paths(:benchmark), do: ["lib", "benchmarks"]
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# Specifies OAuth dependencies.
defp oauth_deps do
oauth_strategy_packages =
System.get_env("OAUTH_CONSUMER_STRATEGIES")
|> to_string()
|> String.split()
|> Enum.map(fn strategy_entry ->
with [_strategy, dependency] <- String.split(strategy_entry, ":") do
dependency
else
[strategy] -> "ueberauth_#{strategy}"
end
end)
for s <- oauth_strategy_packages, do: {String.to_atom(s), ">= 0.0.0"}
end
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.4.8"},
{:tzdata, "~> 0.5.21"},
{:plug_cowboy, "~> 2.0"},
{:phoenix_pubsub, "~> 1.1"},
{:phoenix_ecto, "~> 4.0"},
{:ecto_sql, "~> 3.2"},
{:postgrex, ">= 0.13.5"},
{:oban, "~> 0.8.1"},
{:quantum, "~> 2.3"},
{:gettext, "~> 0.15"},
{:comeonin, "~> 4.1.1"},
{:pbkdf2_elixir, "~> 0.12.3"},
{:trailing_format_plug, "~> 0.0.7"},
{:html_sanitize_ex, "~> 1.3.0"},
{:html_entities, "~> 0.4"},
{:phoenix_html, "~> 2.10"},
{:calendar, "~> 0.17.4"},
{:cachex, "~> 3.0.2"},
{:poison, "~> 3.0", override: true},
{:tesla, "~> 1.3", override: true},
{:jason, "~> 1.0"},
{:mogrify, "~> 0.6.1"},
{:ex_aws, "~> 2.1"},
{:ex_aws_s3, "~> 2.0"},
{:sweet_xml, "~> 0.6.6"},
{:earmark, "~> 1.3"},
{:bbcode, "~> 0.1.1"},
{:ex_machina, "~> 2.3", only: :test},
{:credo, "~> 0.9.3", only: [:dev, :test]},
{:mock, "~> 0.3.3", only: :test},
{:crypt,
git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"},
{:cors_plug, "~> 1.5"},
{:ex_doc, "~> 0.21", only: :dev, runtime: false},
{:web_push_encryption, "~> 0.2.1"},
{:swoosh, "~> 0.23.2"},
{:phoenix_swoosh, "~> 0.2"},
{:gen_smtp, "~> 0.13"},
{:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test},
{:floki, "~> 0.23.0"},
{:ex_syslogger, github: "slashmili/ex_syslogger", tag: "1.4.0"},
{:timex, "~> 3.5"},
{:ueberauth, "~> 0.4"},
{:auto_linker,
git: "https://git.pleroma.social/pleroma/auto_linker.git",
ref: "95e8188490e97505c56636c1379ffdf036c1fdde"},
{:http_signatures,
git: "https://git.pleroma.social/pleroma/http_signatures.git",
ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"},
{:telemetry, "~> 0.3"},
{:poolboy, "~> 1.5"},
{:prometheus_ex, "~> 3.0"},
{:prometheus_plugs, "~> 1.1"},
{:prometheus_phoenix, "~> 1.3"},
{:prometheus_ecto, "~> 1.4"},
{:recon, github: "ferd/recon", tag: "2.4.0"},
{:quack, "~> 0.1.1"},
{:joken, "~> 2.0"},
{:benchee, "~> 1.0"},
{:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
{:ex_rated, "~> 1.3"},
{:ex_const, "~> 0.2"},
{:plug_static_index_html, "~> 1.0.0"},
{:excoveralls, "~> 0.11.1", only: :test},
{:flake_id, "~> 0.1.0"},
{:remote_ip,
git: "https://git.pleroma.social/pleroma/remote_ip.git",
ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"},
{:mox, "~> 0.5", only: :test}
] ++ oauth_deps()
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
"ecto.migrate": ["pleroma.ecto.migrate"],
"ecto.rollback": ["pleroma.ecto.rollback"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"],
docs: ["pleroma.docs", "docs"]
]
end
# Builds a version string made of:
# * the application version
# * a pre-release if ahead of the tag: the describe string (-count-commithash)
# * branch name
# * build metadata:
# * a build name if `PLEROMA_BUILD_NAME` or `:pleroma, :build_name` is defined
# * the mix environment if different than prod
defp version(version) do
identifier_filter = ~r/[^0-9a-z\-]+/i
# Pre-release version, denoted from patch version with a hyphen
{git_tag, git_pre_release} =
with {tag, 0} <-
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true),
tag = String.trim(tag),
{describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]),
describe = String.trim(describe),
ahead <- String.replace(describe, tag, ""),
ahead <- String.trim_leading(ahead, "-") do
{String.replace_prefix(tag, "v", ""), if(ahead != "", do: String.trim(ahead))}
else
_ ->
{commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
{nil, "0-g" <> String.trim(commit_hash)}
end
if git_tag && version != git_tag do
Mix.shell().error(
"Application version #{inspect(version)} does not match git tag #{inspect(git_tag)}"
)
end
# Branch name as pre-release version component, denoted with a dot
branch_name =
with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
branch_name <- String.trim(branch_name),
branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
true <-
!Enum.any?(["master", "HEAD", "release/", "stable"], fn name ->
String.starts_with?(name, branch_name)
end) do
branch_name =
branch_name
|> String.trim()
|> String.replace(identifier_filter, "-")
branch_name
end
build_name =
cond do
name = Application.get_env(:pleroma, :build_name) -> name
name = System.get_env("PLEROMA_BUILD_NAME") -> name
true -> nil
end
env_name = if Mix.env() != :prod, do: to_string(Mix.env())
env_override = System.get_env("PLEROMA_BUILD_ENV")
env_name =
case env_override do
nil -> env_name
env_override when env_override in ["", "prod"] -> nil
env_override -> env_override
end
# Pre-release version, denoted by appending a hyphen
# and a series of dot separated identifiers
pre_release =
[git_pre_release, branch_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join(".")
|> (fn
"" -> nil
string -> "-" <> String.replace(string, identifier_filter, "-")
end).()
# Build metadata, denoted with a plus sign
build_metadata =
[build_name, env_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join(".")
|> (fn
"" -> nil
string -> "+" <> String.replace(string, identifier_filter, "-")
end).()
[version, pre_release, build_metadata]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join()
end
end
diff --git a/priv/repo/migrations/20190710125158_add_following_address_from_source_data.exs b/priv/repo/migrations/20190710125158_add_following_address_from_source_data.exs
index 779aa382e..a5170d521 100644
--- a/priv/repo/migrations/20190710125158_add_following_address_from_source_data.exs
+++ b/priv/repo/migrations/20190710125158_add_following_address_from_source_data.exs
@@ -1,20 +1,24 @@
defmodule Pleroma.Repo.Migrations.AddFollowingAddressFromSourceData do
use Ecto.Migration
import Ecto.Query
alias Pleroma.User
def change do
query =
- User.external_users_query()
+ User.Query.build(%{
+ external: true,
+ legacy_active: true,
+ order_by: :id
+ })
|> select([u], struct(u, [:id, :ap_id, :info]))
Pleroma.Repo.stream(query)
|> Enum.each(fn
%{info: %{source_data: source_data}} = user ->
Ecto.Changeset.cast(user, %{following_address: source_data["following"]}, [
:following_address
])
|> Pleroma.Repo.update()
end)
end
end
diff --git a/priv/repo/migrations/20190711042021_create_safe_jsonb_set.exs b/priv/repo/migrations/20190711042021_create_safe_jsonb_set.exs
new file mode 100644
index 000000000..2f336a5e8
--- /dev/null
+++ b/priv/repo/migrations/20190711042021_create_safe_jsonb_set.exs
@@ -0,0 +1,22 @@
+defmodule Pleroma.Repo.Migrations.CreateSafeJsonbSet do
+ use Ecto.Migration
+ alias Pleroma.User
+
+ def change do
+ execute("""
+ create or replace function safe_jsonb_set(target jsonb, path text[], new_value jsonb, create_missing boolean default true) returns jsonb as $$
+ declare
+ result jsonb;
+ begin
+ result := jsonb_set(target, path, coalesce(new_value, 'null'::jsonb), create_missing);
+ if result is NULL then
+ raise 'jsonb_set tried to wipe the object, please report this incindent to Pleroma bug tracker. https://git.pleroma.social/pleroma/pleroma/issues/new';
+ return target;
+ else
+ return result;
+ end if;
+ end;
+ $$ language plpgsql;
+ """)
+ end
+end
diff --git a/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs b/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs
index bc4e828cc..fc9bf70ba 100644
--- a/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs
+++ b/priv/repo/migrations/20190711042024_copy_muted_to_muted_notifications.exs
@@ -1,10 +1,9 @@
defmodule Pleroma.Repo.Migrations.CopyMutedToMutedNotifications do
use Ecto.Migration
- alias Pleroma.User
def change do
execute(
- "update users set info = jsonb_set(info, '{muted_notifications}', info->'mutes', true) where local = true"
+ "update users set info = safe_jsonb_set(info, '{muted_notifications}', info->'mutes', true) where local = true"
)
end
end
diff --git a/priv/repo/migrations/20191009154606_add_user_info_columns.exs b/priv/repo/migrations/20191009154606_add_user_info_columns.exs
new file mode 100644
index 000000000..22a5a377f
--- /dev/null
+++ b/priv/repo/migrations/20191009154606_add_user_info_columns.exs
@@ -0,0 +1,53 @@
+defmodule Pleroma.Repo.Migrations.AddUsersInfoColumns do
+ use Ecto.Migration
+
+ @jsonb_array_default "'[]'::jsonb"
+
+ def change do
+ alter table(:users) do
+ add_if_not_exists(:banner, :map, default: %{})
+ add_if_not_exists(:background, :map, default: %{})
+ add_if_not_exists(:source_data, :map, default: %{})
+ add_if_not_exists(:note_count, :integer, default: 0)
+ add_if_not_exists(:follower_count, :integer, default: 0)
+ add_if_not_exists(:following_count, :integer, default: nil)
+ add_if_not_exists(:locked, :boolean, default: false, null: false)
+ add_if_not_exists(:confirmation_pending, :boolean, default: false, null: false)
+ add_if_not_exists(:password_reset_pending, :boolean, default: false, null: false)
+ add_if_not_exists(:confirmation_token, :text, default: nil)
+ add_if_not_exists(:default_scope, :string, default: "public")
+ add_if_not_exists(:blocks, {:array, :text}, default: [])
+ add_if_not_exists(:domain_blocks, {:array, :text}, default: [])
+ add_if_not_exists(:mutes, {:array, :text}, default: [])
+ add_if_not_exists(:muted_reblogs, {:array, :text}, default: [])
+ add_if_not_exists(:muted_notifications, {:array, :text}, default: [])
+ add_if_not_exists(:subscribers, {:array, :text}, default: [])
+ add_if_not_exists(:deactivated, :boolean, default: false, null: false)
+ add_if_not_exists(:no_rich_text, :boolean, default: false, null: false)
+ add_if_not_exists(:ap_enabled, :boolean, default: false, null: false)
+ add_if_not_exists(:is_moderator, :boolean, default: false, null: false)
+ add_if_not_exists(:is_admin, :boolean, default: false, null: false)
+ add_if_not_exists(:show_role, :boolean, default: true, null: false)
+ add_if_not_exists(:settings, :map, default: nil)
+ add_if_not_exists(:magic_key, :text, default: nil)
+ add_if_not_exists(:uri, :text, default: nil)
+ add_if_not_exists(:hide_followers_count, :boolean, default: false, null: false)
+ add_if_not_exists(:hide_follows_count, :boolean, default: false, null: false)
+ add_if_not_exists(:hide_followers, :boolean, default: false, null: false)
+ add_if_not_exists(:hide_follows, :boolean, default: false, null: false)
+ add_if_not_exists(:hide_favorites, :boolean, default: true, null: false)
+ add_if_not_exists(:unread_conversation_count, :integer, default: 0)
+ add_if_not_exists(:pinned_activities, {:array, :text}, default: [])
+ add_if_not_exists(:email_notifications, :map, default: %{"digest" => false})
+ add_if_not_exists(:mascot, :map, default: nil)
+ add_if_not_exists(:emoji, :map, default: fragment(@jsonb_array_default))
+ add_if_not_exists(:pleroma_settings_store, :map, default: %{})
+ add_if_not_exists(:fields, :map, default: fragment(@jsonb_array_default))
+ add_if_not_exists(:raw_fields, :map, default: fragment(@jsonb_array_default))
+ add_if_not_exists(:discoverable, :boolean, default: false, null: false)
+ add_if_not_exists(:invisible, :boolean, default: false, null: false)
+ add_if_not_exists(:notification_settings, :map, default: %{})
+ add_if_not_exists(:skip_thread_containment, :boolean, default: false, null: false)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20191009154608_copy_users_info_fields_to_users.exs b/priv/repo/migrations/20191009154608_copy_users_info_fields_to_users.exs
new file mode 100644
index 000000000..cc5ae6920
--- /dev/null
+++ b/priv/repo/migrations/20191009154608_copy_users_info_fields_to_users.exs
@@ -0,0 +1,145 @@
+defmodule Pleroma.Repo.Migrations.CopyUsersInfoFieldsToUsers do
+ use Ecto.Migration
+
+ @jsonb_array_default "'[]'::jsonb"
+
+ @info_fields [
+ :banner,
+ :background,
+ :source_data,
+ :note_count,
+ :follower_count,
+ :following_count,
+ :locked,
+ :confirmation_pending,
+ :password_reset_pending,
+ :confirmation_token,
+ :default_scope,
+ :blocks,
+ :domain_blocks,
+ :mutes,
+ :muted_reblogs,
+ :muted_notifications,
+ :subscribers,
+ :deactivated,
+ :no_rich_text,
+ :ap_enabled,
+ :is_moderator,
+ :is_admin,
+ :show_role,
+ :settings,
+ :magic_key,
+ :uri,
+ :hide_followers_count,
+ :hide_follows_count,
+ :hide_followers,
+ :hide_follows,
+ :hide_favorites,
+ :unread_conversation_count,
+ :pinned_activities,
+ :email_notifications,
+ :mascot,
+ :emoji,
+ :pleroma_settings_store,
+ :fields,
+ :raw_fields,
+ :discoverable,
+ :invisible,
+ :skip_thread_containment,
+ :notification_settings
+ ]
+
+ @jsonb_fields [
+ :banner,
+ :background,
+ :source_data,
+ :settings,
+ :email_notifications,
+ :mascot,
+ :pleroma_settings_store,
+ :notification_settings
+ ]
+
+ @array_jsonb_fields [:emoji, :fields, :raw_fields]
+
+ @int_fields [:note_count, :follower_count, :following_count, :unread_conversation_count]
+
+ @boolean_fields [
+ :locked,
+ :confirmation_pending,
+ :password_reset_pending,
+ :deactivated,
+ :no_rich_text,
+ :ap_enabled,
+ :is_moderator,
+ :is_admin,
+ :show_role,
+ :hide_followers_count,
+ :hide_follows_count,
+ :hide_followers,
+ :hide_follows,
+ :hide_favorites,
+ :discoverable,
+ :invisible,
+ :skip_thread_containment
+ ]
+
+ @array_text_fields [
+ :blocks,
+ :domain_blocks,
+ :mutes,
+ :muted_reblogs,
+ :muted_notifications,
+ :subscribers,
+ :pinned_activities
+ ]
+
+ def change do
+ if direction() == :up do
+ sets =
+ for f <- @info_fields do
+ set_field = "#{f} ="
+
+ # Coercion of null::jsonb to NULL
+ jsonb = "case when info->>'#{f}' IS NULL then null else info->'#{f}' end"
+
+ cond do
+ f in @jsonb_fields ->
+ "#{set_field} #{jsonb}"
+
+ f in @array_jsonb_fields ->
+ "#{set_field} coalesce(#{jsonb}, #{@jsonb_array_default})"
+
+ f in @int_fields ->
+ "#{set_field} (info->>'#{f}')::int"
+
+ f in @boolean_fields ->
+ "#{set_field} coalesce((info->>'#{f}')::boolean, false)"
+
+ f in @array_text_fields ->
+ "#{set_field} ARRAY(SELECT jsonb_array_elements_text(#{jsonb}))"
+
+ true ->
+ "#{set_field} info->>'#{f}'"
+ end
+ end
+ |> Enum.join(", ")
+
+ execute("update users set " <> sets)
+
+ for index_name <- [
+ :users_deactivated_index,
+ :users_is_moderator_index,
+ :users_is_admin_index,
+ :users_subscribers_index
+ ] do
+ drop_if_exists(index(:users, [], name: index_name))
+ end
+ end
+
+ create_if_not_exists(index(:users, [:deactivated]))
+ create_if_not_exists(index(:users, [:is_moderator]))
+ create_if_not_exists(index(:users, [:is_admin]))
+ create_if_not_exists(index(:users, [:subscribers]))
+ end
+end
diff --git a/priv/repo/migrations/20191014181019_create_markers.exs b/priv/repo/migrations/20191014181019_create_markers.exs
new file mode 100644
index 000000000..c717831ba
--- /dev/null
+++ b/priv/repo/migrations/20191014181019_create_markers.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.CreateMarkers do
+ use Ecto.Migration
+
+ def change do
+ create_if_not_exists table(:markers) do
+ add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
+ add(:timeline, :string, default: "", null: false)
+ add(:last_read_id, :string, default: "", null: false)
+ add(:lock_version, :integer, default: 0, null: false)
+ timestamps()
+ end
+
+ create_if_not_exists(unique_index(:markers, [:user_id, :timeline]))
+ end
+end
diff --git a/priv/repo/migrations/20191017225002_drop_websub_tables.exs b/priv/repo/migrations/20191017225002_drop_websub_tables.exs
new file mode 100644
index 000000000..4cf67a59c
--- /dev/null
+++ b/priv/repo/migrations/20191017225002_drop_websub_tables.exs
@@ -0,0 +1,10 @@
+defmodule Pleroma.Repo.Migrations.DropWebsubTables do
+ use Ecto.Migration
+
+ def up do
+ drop_if_exists(table(:websub_client_subscriptions))
+ drop_if_exists(table(:websub_server_subscriptions))
+ end
+
+ def down, do: :noop
+end
diff --git a/priv/repo/migrations/20191026190317_set_not_null_for_activities.exs b/priv/repo/migrations/20191026190317_set_not_null_for_activities.exs
new file mode 100644
index 000000000..9b66f3cff
--- /dev/null
+++ b/priv/repo/migrations/20191026190317_set_not_null_for_activities.exs
@@ -0,0 +1,17 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForActivities do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE activities
+ ALTER COLUMN data SET NOT NULL,
+ ALTER COLUMN local SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE activities
+ ALTER COLUMN data DROP NOT NULL,
+ ALTER COLUMN local DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026190415_set_not_null_for_activity_expirations.exs b/priv/repo/migrations/20191026190415_set_not_null_for_activity_expirations.exs
new file mode 100644
index 000000000..e41c69e4c
--- /dev/null
+++ b/priv/repo/migrations/20191026190415_set_not_null_for_activity_expirations.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForActivityExpirations do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE activity_expirations
+ ALTER COLUMN activity_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE activity_expirations
+ ALTER COLUMN activity_id DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026190500_set_not_null_for_apps.exs b/priv/repo/migrations/20191026190500_set_not_null_for_apps.exs
new file mode 100644
index 000000000..a6a44ddfe
--- /dev/null
+++ b/priv/repo/migrations/20191026190500_set_not_null_for_apps.exs
@@ -0,0 +1,17 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForApps do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE apps
+ ALTER COLUMN client_name SET NOT NULL,
+ ALTER COLUMN redirect_uris SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE apps
+ ALTER COLUMN client_name DROP NOT NULL,
+ ALTER COLUMN redirect_uris DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026190533_set_not_null_for_bookmarks.exs b/priv/repo/migrations/20191026190533_set_not_null_for_bookmarks.exs
new file mode 100644
index 000000000..5f3224003
--- /dev/null
+++ b/priv/repo/migrations/20191026190533_set_not_null_for_bookmarks.exs
@@ -0,0 +1,17 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForBookmarks do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE bookmarks
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN activity_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE bookmarks
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN activity_id DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026190622_set_not_null_for_config.exs b/priv/repo/migrations/20191026190622_set_not_null_for_config.exs
new file mode 100644
index 000000000..204272442
--- /dev/null
+++ b/priv/repo/migrations/20191026190622_set_not_null_for_config.exs
@@ -0,0 +1,17 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForConfig do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE config
+ ALTER COLUMN key SET NOT NULL,
+ ALTER COLUMN value SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE config
+ ALTER COLUMN key DROP NOT NULL,
+ ALTER COLUMN value DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026190712_set_not_null_for_conversation_participation_recipient_ships.exs b/priv/repo/migrations/20191026190712_set_not_null_for_conversation_participation_recipient_ships.exs
new file mode 100644
index 000000000..a5ab1d3c9
--- /dev/null
+++ b/priv/repo/migrations/20191026190712_set_not_null_for_conversation_participation_recipient_ships.exs
@@ -0,0 +1,17 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForConversationParticipationRecipientShips do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE conversation_participation_recipient_ships
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN participation_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE conversation_participation_recipient_ships
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN participation_id DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026190759_set_not_null_for_conversation_participations.exs b/priv/repo/migrations/20191026190759_set_not_null_for_conversation_participations.exs
new file mode 100644
index 000000000..cabb1f29f
--- /dev/null
+++ b/priv/repo/migrations/20191026190759_set_not_null_for_conversation_participations.exs
@@ -0,0 +1,19 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForConversationParticipations do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE conversation_participations
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN conversation_id SET NOT NULL,
+ ALTER COLUMN read SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE conversation_participations
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN conversation_id DROP NOT NULL,
+ ALTER COLUMN read DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026190841_set_not_null_for_filters.exs b/priv/repo/migrations/20191026190841_set_not_null_for_filters.exs
new file mode 100644
index 000000000..52d7e687a
--- /dev/null
+++ b/priv/repo/migrations/20191026190841_set_not_null_for_filters.exs
@@ -0,0 +1,19 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForFilters do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE filters
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN filter_id SET NOT NULL,
+ ALTER COLUMN whole_word SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE filters
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN filter_id DROP NOT NULL,
+ ALTER COLUMN whole_word DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191023_set_not_null_for_instances.exs b/priv/repo/migrations/20191026191023_set_not_null_for_instances.exs
new file mode 100644
index 000000000..4c2560da5
--- /dev/null
+++ b/priv/repo/migrations/20191026191023_set_not_null_for_instances.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForInstances do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE instances
+ ALTER COLUMN host SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE instances
+ ALTER COLUMN host DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191100_set_not_null_for_lists.exs b/priv/repo/migrations/20191026191100_set_not_null_for_lists.exs
new file mode 100644
index 000000000..40b8b136a
--- /dev/null
+++ b/priv/repo/migrations/20191026191100_set_not_null_for_lists.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForLists do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE lists
+ ALTER COLUMN user_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE lists
+ ALTER COLUMN user_id DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191134_set_not_null_for_markers.exs b/priv/repo/migrations/20191026191134_set_not_null_for_markers.exs
new file mode 100644
index 000000000..7d7b73e7b
--- /dev/null
+++ b/priv/repo/migrations/20191026191134_set_not_null_for_markers.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForMarkers do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE markers
+ ALTER COLUMN user_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE markers
+ ALTER COLUMN user_id DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191218_set_not_null_for_moderation_log.exs b/priv/repo/migrations/20191026191218_set_not_null_for_moderation_log.exs
new file mode 100644
index 000000000..7238ca7f5
--- /dev/null
+++ b/priv/repo/migrations/20191026191218_set_not_null_for_moderation_log.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForModerationLog do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE moderation_log
+ ALTER COLUMN data SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE moderation_log
+ ALTER COLUMN data DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191249_set_not_null_for_notifications.exs b/priv/repo/migrations/20191026191249_set_not_null_for_notifications.exs
new file mode 100644
index 000000000..7e2976bab
--- /dev/null
+++ b/priv/repo/migrations/20191026191249_set_not_null_for_notifications.exs
@@ -0,0 +1,17 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForNotifications do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE notifications
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN seen SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE notifications
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN seen DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191328_set_not_null_for_oauth_authorizations.exs b/priv/repo/migrations/20191026191328_set_not_null_for_oauth_authorizations.exs
new file mode 100644
index 000000000..bc6b36e69
--- /dev/null
+++ b/priv/repo/migrations/20191026191328_set_not_null_for_oauth_authorizations.exs
@@ -0,0 +1,19 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForOauthAuthorizations do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE oauth_authorizations
+ ALTER COLUMN app_id SET NOT NULL,
+ ALTER COLUMN token SET NOT NULL,
+ ALTER COLUMN used SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE oauth_authorizations
+ ALTER COLUMN app_id DROP NOT NULL,
+ ALTER COLUMN token DROP NOT NULL,
+ ALTER COLUMN used DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191401_set_not_null_for_oauth_tokens.exs b/priv/repo/migrations/20191026191401_set_not_null_for_oauth_tokens.exs
new file mode 100644
index 000000000..fe67db8cc
--- /dev/null
+++ b/priv/repo/migrations/20191026191401_set_not_null_for_oauth_tokens.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForOauthTokens do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE oauth_tokens
+ ALTER COLUMN app_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE oauth_tokens
+ ALTER COLUMN app_id DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191442_set_not_null_for_objects.exs b/priv/repo/migrations/20191026191442_set_not_null_for_objects.exs
new file mode 100644
index 000000000..59e89d6da
--- /dev/null
+++ b/priv/repo/migrations/20191026191442_set_not_null_for_objects.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForObjects do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE objects
+ ALTER COLUMN data SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE objects
+ ALTER COLUMN data DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191524_set_not_null_for_password_reset_tokens.exs b/priv/repo/migrations/20191026191524_set_not_null_for_password_reset_tokens.exs
new file mode 100644
index 000000000..51efadbd3
--- /dev/null
+++ b/priv/repo/migrations/20191026191524_set_not_null_for_password_reset_tokens.exs
@@ -0,0 +1,19 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForPasswordResetTokens do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE password_reset_tokens
+ ALTER COLUMN token SET NOT NULL,
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN used SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE password_reset_tokens
+ ALTER COLUMN token DROP NOT NULL,
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN used DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191603_set_not_null_for_push_subscriptions.exs b/priv/repo/migrations/20191026191603_set_not_null_for_push_subscriptions.exs
new file mode 100644
index 000000000..0f3a73067
--- /dev/null
+++ b/priv/repo/migrations/20191026191603_set_not_null_for_push_subscriptions.exs
@@ -0,0 +1,25 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForPushSubscriptions do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE push_subscriptions
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN token_id SET NOT NULL,
+ ALTER COLUMN endpoint SET NOT NULL,
+ ALTER COLUMN key_p256dh SET NOT NULL,
+ ALTER COLUMN key_auth SET NOT NULL,
+ ALTER COLUMN data SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE push_subscriptions
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN token_id DROP NOT NULL,
+ ALTER COLUMN endpoint DROP NOT NULL,
+ ALTER COLUMN key_p256dh DROP NOT NULL,
+ ALTER COLUMN key_auth DROP NOT NULL,
+ ALTER COLUMN data DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191635_set_not_null_for_registrations.exs b/priv/repo/migrations/20191026191635_set_not_null_for_registrations.exs
new file mode 100644
index 000000000..ddfbf4c5e
--- /dev/null
+++ b/priv/repo/migrations/20191026191635_set_not_null_for_registrations.exs
@@ -0,0 +1,19 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForRegistrations do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE registrations
+ ALTER COLUMN provider SET NOT NULL,
+ ALTER COLUMN uid SET NOT NULL,
+ ALTER COLUMN info SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE registrations
+ ALTER COLUMN provider DROP NOT NULL,
+ ALTER COLUMN uid DROP NOT NULL,
+ ALTER COLUMN info DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191711_set_not_null_for_scheduled_activities.exs b/priv/repo/migrations/20191026191711_set_not_null_for_scheduled_activities.exs
new file mode 100644
index 000000000..f1830c8c3
--- /dev/null
+++ b/priv/repo/migrations/20191026191711_set_not_null_for_scheduled_activities.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForScheduledActivities do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE scheduled_activities
+ ALTER COLUMN user_id SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE scheduled_activities
+ ALTER COLUMN user_id DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191753_set_not_null_for_thread_mutes.exs b/priv/repo/migrations/20191026191753_set_not_null_for_thread_mutes.exs
new file mode 100644
index 000000000..daa7ce314
--- /dev/null
+++ b/priv/repo/migrations/20191026191753_set_not_null_for_thread_mutes.exs
@@ -0,0 +1,17 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForThreadMutes do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE thread_mutes
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN context SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE thread_mutes
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN context DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191826_set_not_null_for_user_invite_tokens.exs b/priv/repo/migrations/20191026191826_set_not_null_for_user_invite_tokens.exs
new file mode 100644
index 000000000..836544f7f
--- /dev/null
+++ b/priv/repo/migrations/20191026191826_set_not_null_for_user_invite_tokens.exs
@@ -0,0 +1,19 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForUserInviteTokens do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ execute("ALTER TABLE user_invite_tokens
+ ALTER COLUMN used SET NOT NULL,
+ ALTER COLUMN uses SET NOT NULL,
+ ALTER COLUMN invite_type SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE user_invite_tokens
+ ALTER COLUMN used DROP NOT NULL,
+ ALTER COLUMN uses DROP NOT NULL,
+ ALTER COLUMN invite_type DROP NOT NULL")
+ end
+end
diff --git a/priv/repo/migrations/20191026191910_set_not_null_for_users.exs b/priv/repo/migrations/20191026191910_set_not_null_for_users.exs
new file mode 100644
index 000000000..f145a89ab
--- /dev/null
+++ b/priv/repo/migrations/20191026191910_set_not_null_for_users.exs
@@ -0,0 +1,46 @@
+defmodule Pleroma.Repo.Migrations.SetNotNullForUsers do
+ use Ecto.Migration
+
+ # modify/3 function will require index recreation, so using execute/1 instead
+
+ def up do
+ # irreversible
+ execute("UPDATE users SET follower_count = 0 WHERE follower_count IS NULL")
+
+ execute("ALTER TABLE users
+ ALTER COLUMN following SET NOT NULL,
+ ALTER COLUMN local SET NOT NULL,
+ ALTER COLUMN source_data SET NOT NULL,
+ ALTER COLUMN note_count SET NOT NULL,
+ ALTER COLUMN follower_count SET NOT NULL,
+ ALTER COLUMN blocks SET NOT NULL,
+ ALTER COLUMN domain_blocks SET NOT NULL,
+ ALTER COLUMN mutes SET NOT NULL,
+ ALTER COLUMN muted_reblogs SET NOT NULL,
+ ALTER COLUMN muted_notifications SET NOT NULL,
+ ALTER COLUMN subscribers SET NOT NULL,
+ ALTER COLUMN pinned_activities SET NOT NULL,
+ ALTER COLUMN emoji SET NOT NULL,
+ ALTER COLUMN fields SET NOT NULL,
+ ALTER COLUMN raw_fields SET NOT NULL")
+ end
+
+ def down do
+ execute("ALTER TABLE users
+ ALTER COLUMN following DROP NOT NULL,
+ ALTER COLUMN local DROP NOT NULL,
+ ALTER COLUMN source_data DROP NOT NULL,
+ ALTER COLUMN note_count DROP NOT NULL,
+ ALTER COLUMN follower_count DROP NOT NULL,
+ ALTER COLUMN blocks DROP NOT NULL,
+ ALTER COLUMN domain_blocks DROP NOT NULL,
+ ALTER COLUMN mutes DROP NOT NULL,
+ ALTER COLUMN muted_reblogs DROP NOT NULL,
+ ALTER COLUMN muted_notifications DROP NOT NULL,
+ ALTER COLUMN subscribers DROP NOT NULL,
+ ALTER COLUMN pinned_activities DROP NOT NULL,
+ ALTER COLUMN emoji DROP NOT NULL,
+ ALTER COLUMN fields DROP NOT NULL,
+ ALTER COLUMN raw_fields DROP NOT NULL")
+ end
+end
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index 1cfcb7ec7..c8e69cab5 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -1,33 +1,34 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"Emoji": "toot:Emoji",
"Hashtag": "as:Hashtag",
"PropertyValue": "schema:PropertyValue",
"atomUri": "ostatus:atomUri",
"conversation": {
"@id": "ostatus:conversation",
"@type": "@id"
},
"discoverable": "toot:discoverable",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"ostatus": "http://ostatus.org#",
"schema": "http://schema.org#",
"toot": "http://joinmastodon.org/ns#",
"value": "schema:value",
"sensitive": "as:sensitive",
"litepub": "http://litepub.social/ns#",
+ "invisible": "litepub:invisible",
"directMessage": "litepub:directMessage",
"listMessage": {
"@id": "litepub:listMessage",
"@type": "@id"
},
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"
}
}
]
}
diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl
index 90f87a990..87c486514 100755
--- a/rel/files/bin/pleroma_ctl
+++ b/rel/files/bin/pleroma_ctl
@@ -1,157 +1,161 @@
#!/bin/sh
# XXX: This should be removed when elixir's releases get custom command support
detect_flavour() {
arch="$(uname -m)"
if [ "$arch" = "x86_64" ]; then
arch="amd64"
elif [ "$arch" = "armv7l" ]; then
arch="arm"
elif [ "$arch" = "aarch64" ]; then
arch="arm64"
else
echo "Unsupported arch: $arch" >&2
exit 1
fi
if getconf GNU_LIBC_VERSION >/dev/null; then
libc_postfix=""
elif [ "$(ldd 2>&1 | head -c 9)" = "musl libc" ]; then
libc_postfix="-musl"
elif [ "$(find /lib/libc.musl* | wc -l)" ]; then
libc_postfix="-musl"
else
echo "Unsupported libc" >&2
exit 1
fi
echo "$arch$libc_postfix"
}
detect_branch() {
version="$(cut -d' ' -f2 <"$RELEASE_ROOT"/releases/start_erl.data)"
# Expected format: major.minor.patch_version(-number_of_commits_ahead_of_tag-gcommit_hash).branch
branch="$(echo "$version" | cut -d'.' -f 4)"
if [ "$branch" = "develop" ]; then
echo "develop"
elif [ "$branch" = "" ]; then
echo "stable"
else
# Note: branch name in version is of SemVer format and may only contain [0-9a-zA-Z-] symbols —
# if supporting releases for more branches, need to ensure they contain only these symbols.
echo "Can't detect the branch automatically, please specify it by using the --branch option." >&2
exit 1
fi
}
update() {
set -e
NO_RM=false
while echo "$1" | grep "^-" >/dev/null; do
case "$1" in
--zip-url)
FULL_URI="$2"
shift 2
;;
--no-rm)
NO_RM=true
shift
;;
--flavour)
FLAVOUR="$2"
shift 2
;;
--branch)
BRANCH="$2"
shift 2
;;
--tmp-dir)
TMP_DIR="$2"
shift 2
;;
-*)
echo "invalid option: $1" 1>&2
shift
;;
esac
done
RELEASE_ROOT=$(dirname "$SCRIPTPATH")
uri="https://git.pleroma.social"
project_id="2"
project_branch="${BRANCH:-$(detect_branch)}"
flavour="${FLAVOUR:-$(detect_flavour)}"
tmp="${TMP_DIR:-/tmp}"
artifact="$tmp/pleroma.zip"
full_uri="${FULL_URI:-${uri}/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=${flavour}}"
echo "Downloading the artifact from ${full_uri} to ${artifact}"
curl "$full_uri" -o "${artifact}"
echo "Unpacking ${artifact} to ${tmp}"
unzip -q "$artifact" -d "$tmp"
echo "Copying files over to $RELEASE_ROOT"
if [ "$NO_RM" = false ]; then
echo "Removing files from the previous release"
rm -r "${RELEASE_ROOT:-?}"/*
fi
cp -rf "$tmp/release"/* "$RELEASE_ROOT"
echo "Removing temporary files"
rm -r "$tmp/release"
rm "$artifact"
echo "Done! Please refer to the changelog/release notes for changes and update instructions"
set +e
}
if [ -z "$1" ] || [ "$1" = "help" ]; then
# TODO: Just list the commands on `pleroma_ctl help` and output help for the individual command on `pleroma_ctl help $COMMAND`
echo "Usage: $(basename "$0") COMMAND [ARGS]
The known commands are:
create
Create database schema (needs to be executed only once)
migrate
Execute database migrations (needs to be done after updates)
rollback [VERSION]
Rollback database migrations (needs to be done before downgrading)
update [OPTIONS]
Update the instance.
Options:
--branch Update to a specified branch, instead of the latest version of the current one.
--flavour Update to a specified flavour (CPU architecture+libc), instead of the current one.
--zip-url Get the release from a specified url. If set, renders the previous 2 options inactive.
--no-rm Do not erase previous release's files.
--tmp-dir Download the temporary files to a specified directory.
and any mix tasks under Pleroma namespace, for example \`mix pleroma.user COMMAND\` is
equivalent to \`$(basename "$0") user COMMAND\`
By default pleroma_ctl will try calling into a running instance to execute non migration-related commands,
if for some reason this is undesired, set PLEROMA_CTL_RPC_DISABLED environment variable.
"
else
SCRIPT=$(readlink -f "$0")
SCRIPTPATH=$(dirname "$SCRIPT")
FULL_ARGS="$*"
ACTION="$1"
- shift
-
- if [ "$(echo \"$1\" | grep \"^-\" >/dev/null)" = false ]; then
- SUBACTION="$1"
+ if [ $# -gt 0 ]; then
shift
fi
+ echo "$1" | grep "^-" >/dev/null
+ if [ $? -eq 1 ]; then
+ SUBACTION="$1"
+ if [ $# -gt 0 ]; then
+ shift
+ fi
+ fi
if [ "$ACTION" = "update" ]; then
update "$@"
elif [ "$ACTION" = "migrate" ] || [ "$ACTION" = "rollback" ] || [ "$ACTION" = "create" ] || [ "$ACTION $SUBACTION" = "instance gen" ] || [ "$PLEROMA_CTL_RPC_DISABLED" = true ]; then
"$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")'
else
"$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")'
fi
fi
diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs
index f430bdf75..863270022 100644
--- a/test/conversation/participation_test.exs
+++ b/test/conversation/participation_test.exs
@@ -1,172 +1,349 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation.ParticipationTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Conversation.Participation
alias Pleroma.User
alias Pleroma.Web.CommonAPI
test "getting a participation will also preload things" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
[participation] = Participation.for_user(user)
participation = Participation.get(participation.id, preload: [:conversation])
assert %Pleroma.Conversation{} = participation.conversation
end
+ test "for a new conversation or a reply, it doesn't mark the author's participation as unread" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, _} =
+ CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
+
+ user = User.get_cached_by_id(user.id)
+ other_user = User.get_cached_by_id(other_user.id)
+
+ [%{read: true}] = Participation.for_user(user)
+ [%{read: false} = participation] = Participation.for_user(other_user)
+
+ assert User.get_cached_by_id(user.id).unread_conversation_count == 0
+ assert User.get_cached_by_id(other_user.id).unread_conversation_count == 1
+
+ {:ok, _} =
+ CommonAPI.post(other_user, %{
+ "status" => "Hey @#{user.nickname}.",
+ "visibility" => "direct",
+ "in_reply_to_conversation_id" => participation.id
+ })
+
+ user = User.get_cached_by_id(user.id)
+ other_user = User.get_cached_by_id(other_user.id)
+
+ [%{read: false}] = Participation.for_user(user)
+ [%{read: true}] = Participation.for_user(other_user)
+
+ assert User.get_cached_by_id(user.id).unread_conversation_count == 1
+ assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0
+ end
+
test "for a new conversation, it sets the recipents of the participation" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
user = User.get_cached_by_id(user.id)
- other_user = User.get_cached_by_id(user.id)
+ other_user = User.get_cached_by_id(other_user.id)
[participation] = Participation.for_user(user)
participation = Pleroma.Repo.preload(participation, :recipients)
assert length(participation.recipients) == 2
assert user in participation.recipients
assert other_user in participation.recipients
# Mentioning another user in the same conversation will not add a new recipients.
{:ok, _activity} =
CommonAPI.post(user, %{
"in_reply_to_status_id" => activity.id,
"status" => "Hey @#{third_user.nickname}.",
"visibility" => "direct"
})
[participation] = Participation.for_user(user)
participation = Pleroma.Repo.preload(participation, :recipients)
assert length(participation.recipients) == 2
end
test "it creates a participation for a conversation and a user" do
user = insert(:user)
conversation = insert(:conversation)
{:ok, %Participation{} = participation} =
Participation.create_for_user_and_conversation(user, conversation)
assert participation.user_id == user.id
assert participation.conversation_id == conversation.id
:timer.sleep(1000)
# Creating again returns the same participation
{:ok, %Participation{} = participation_two} =
Participation.create_for_user_and_conversation(user, conversation)
assert participation.id == participation_two.id
refute participation.updated_at == participation_two.updated_at
end
test "recreating an existing participations sets it to unread" do
participation = insert(:participation, %{read: true})
{:ok, participation} =
Participation.create_for_user_and_conversation(
participation.user,
participation.conversation
)
refute participation.read
end
test "it marks a participation as read" do
participation = insert(:participation, %{read: false})
{:ok, participation} = Participation.mark_as_read(participation)
assert participation.read
end
test "it marks a participation as unread" do
participation = insert(:participation, %{read: true})
{:ok, participation} = Participation.mark_as_unread(participation)
refute participation.read
end
+ test "it marks all the user's participations as read" do
+ user = insert(:user)
+ other_user = insert(:user)
+ participation1 = insert(:participation, %{read: false, user: user})
+ participation2 = insert(:participation, %{read: false, user: user})
+ participation3 = insert(:participation, %{read: false, user: other_user})
+
+ {:ok, _, [%{read: true}, %{read: true}]} = Participation.mark_all_as_read(user)
+
+ assert Participation.get(participation1.id).read == true
+ assert Participation.get(participation2.id).read == true
+ assert Participation.get(participation3.id).read == false
+ end
+
test "gets all the participations for a user, ordered by updated at descending" do
user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"})
:timer.sleep(1000)
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"})
:timer.sleep(1000)
{:ok, activity_three} =
CommonAPI.post(user, %{
"status" => "x",
"visibility" => "direct",
"in_reply_to_status_id" => activity_one.id
})
assert [participation_one, participation_two] = Participation.for_user(user)
object2 = Pleroma.Object.normalize(activity_two)
object3 = Pleroma.Object.normalize(activity_three)
user = Repo.get(Pleroma.User, user.id)
assert participation_one.conversation.ap_id == object3.data["context"]
assert participation_two.conversation.ap_id == object2.data["context"]
assert participation_one.conversation.users == [user]
# Pagination
assert [participation_one] = Participation.for_user(user, %{"limit" => 1})
assert participation_one.conversation.ap_id == object3.data["context"]
# With last_activity_id
assert [participation_one] =
Participation.for_user_with_last_activity_id(user, %{"limit" => 1})
assert participation_one.last_activity_id == activity_three.id
end
test "Doesn't die when the conversation gets empty" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
[participation] = Participation.for_user_with_last_activity_id(user)
assert participation.last_activity_id == activity.id
{:ok, _} = CommonAPI.delete(activity.id, user)
[] = Participation.for_user_with_last_activity_id(user)
end
test "it sets recipients, always keeping the owner of the participation even when not explicitly set" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
[participation] = Participation.for_user_with_last_activity_id(user)
participation = Repo.preload(participation, :recipients)
user = User.get_cached_by_id(user.id)
assert participation.recipients |> length() == 1
assert user in participation.recipients
{:ok, participation} = Participation.set_recipients(participation, [other_user.id])
assert participation.recipients |> length() == 2
assert user in participation.recipients
assert other_user in participation.recipients
end
+
+ describe "blocking" do
+ test "when the user blocks a recipient, the existing conversations with them are marked as read" do
+ blocker = insert(:user)
+ blocked = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, _direct1} =
+ CommonAPI.post(third_user, %{
+ "status" => "Hi @#{blocker.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, _direct2} =
+ CommonAPI.post(third_user, %{
+ "status" => "Hi @#{blocker.nickname}, @#{blocked.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, _direct3} =
+ CommonAPI.post(blocked, %{
+ "status" => "Hi @#{blocker.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, _direct4} =
+ CommonAPI.post(blocked, %{
+ "status" => "Hi @#{blocker.nickname}, @#{third_user.nickname}",
+ "visibility" => "direct"
+ })
+
+ assert [%{read: false}, %{read: false}, %{read: false}, %{read: false}] =
+ Participation.for_user(blocker)
+
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4
+
+ {:ok, blocker} = User.block(blocker, blocked)
+
+ # The conversations with the blocked user are marked as read
+ assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] =
+ Participation.for_user(blocker)
+
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 1
+
+ # The conversation is not marked as read for the blocked user
+ assert [_, _, %{read: false}] = Participation.for_user(blocked)
+ assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
+
+ # The conversation is not marked as read for the third user
+ assert [%{read: false}, _, _] = Participation.for_user(third_user)
+ assert User.get_cached_by_id(third_user.id).unread_conversation_count == 1
+ end
+
+ test "the new conversation with the blocked user is not marked as unread " do
+ blocker = insert(:user)
+ blocked = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, blocker} = User.block(blocker, blocked)
+
+ # When the blocked user is the author
+ {:ok, _direct1} =
+ CommonAPI.post(blocked, %{
+ "status" => "Hi @#{blocker.nickname}",
+ "visibility" => "direct"
+ })
+
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ # When the blocked user is a recipient
+ {:ok, _direct2} =
+ CommonAPI.post(third_user, %{
+ "status" => "Hi @#{blocker.nickname}, @#{blocked.nickname}",
+ "visibility" => "direct"
+ })
+
+ assert [%{read: true}, %{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ assert [%{read: false}, _] = Participation.for_user(blocked)
+ assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
+ end
+
+ test "the conversation with the blocked user is not marked as unread on a reply" do
+ blocker = insert(:user)
+ blocked = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, _direct1} =
+ CommonAPI.post(blocker, %{
+ "status" => "Hi @#{third_user.nickname}, @#{blocked.nickname}",
+ "visibility" => "direct"
+ })
+
+ {:ok, blocker} = User.block(blocker, blocked)
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ assert [blocked_participation] = Participation.for_user(blocked)
+
+ # When it's a reply from the blocked user
+ {:ok, _direct2} =
+ CommonAPI.post(blocked, %{
+ "status" => "reply",
+ "visibility" => "direct",
+ "in_reply_to_conversation_id" => blocked_participation.id
+ })
+
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ assert [third_user_participation] = Participation.for_user(third_user)
+
+ # When it's a reply from the third user
+ {:ok, _direct3} =
+ CommonAPI.post(third_user, %{
+ "status" => "reply",
+ "visibility" => "direct",
+ "in_reply_to_conversation_id" => third_user_participation.id
+ })
+
+ assert [%{read: true}] = Participation.for_user(blocker)
+ assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
+
+ # Marked as unread for the blocked user
+ assert [%{read: false}] = Participation.for_user(blocked)
+ assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
+ end
+ end
end
diff --git a/test/daemons/digest_email_daemon_test.exs b/test/daemons/digest_email_daemon_test.exs
index 3168f3b9a..faf592d5f 100644
--- a/test/daemons/digest_email_daemon_test.exs
+++ b/test/daemons/digest_email_daemon_test.exs
@@ -1,35 +1,35 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.DigestEmailDaemonTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Daemons.DigestEmailDaemon
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.CommonAPI
test "it sends digest emails" do
user = insert(:user)
date =
Timex.now()
|> Timex.shift(days: -10)
|> Timex.to_naive_datetime()
user2 = insert(:user, last_digest_emailed_at: date)
- User.switch_email_notifications(user2, "digest", true)
+ {:ok, _} = User.switch_email_notifications(user2, "digest", true)
CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}!"})
DigestEmailDaemon.perform()
ObanHelpers.perform_all()
# Performing job(s) enqueued at previous step
ObanHelpers.perform_all()
assert_received {:email, email}
assert email.to == [{user2.name, user2.email}]
assert email.subject == "Your digest from #{Pleroma.Config.get(:instance)[:name]}"
end
end
diff --git a/test/emails/user_email_test.exs b/test/emails/user_email_test.exs
index 963565f7c..9e145977e 100644
--- a/test/emails/user_email_test.exs
+++ b/test/emails/user_email_test.exs
@@ -1,48 +1,48 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.UserEmailTest do
use Pleroma.DataCase
alias Pleroma.Emails.UserEmail
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router
import Pleroma.Factory
test "build password reset email" do
config = Pleroma.Config.get(:instance)
user = insert(:user)
email = UserEmail.password_reset_email(user, "test_token")
assert email.from == {config[:name], config[:notify_email]}
assert email.to == [{user.name, user.email}]
assert email.subject == "Password reset"
assert email.html_body =~ Router.Helpers.reset_password_url(Endpoint, :reset, "test_token")
end
test "build user invitation email" do
config = Pleroma.Config.get(:instance)
user = insert(:user)
token = %Pleroma.UserInviteToken{token: "test-token"}
email = UserEmail.user_invitation_email(user, token, "test@test.com", "Jonh")
assert email.from == {config[:name], config[:notify_email]}
assert email.subject == "Invitation to Pleroma"
assert email.to == [{"Jonh", "test@test.com"}]
assert email.html_body =~
Router.Helpers.redirect_url(Endpoint, :registration_page, token.token)
end
test "build account confirmation email" do
config = Pleroma.Config.get(:instance)
- user = insert(:user, info: %Pleroma.User.Info{confirmation_token: "conf-token"})
+ user = insert(:user, confirmation_token: "conf-token")
email = UserEmail.account_confirmation_email(user)
assert email.from == {config[:name], config[:notify_email]}
assert email.to == [{user.name, user.email}]
assert email.subject == "#{config[:name]} account confirmation"
assert email.html_body =~
Router.Helpers.confirm_email_url(Endpoint, :confirm_email, user.id, "conf-token")
end
end
diff --git a/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json b/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json
new file mode 100644
index 000000000..4b7b4df44
--- /dev/null
+++ b/test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://shitposter.club/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://shitposter.club/users/moonman","attachment":[],"attributedTo":"https://shitposter.club/users/moonman","cc":["https://shitposter.club/users/moonman/followers"],"content":"@<a href=\"https://shitposter.club/users/9655\" class=\"h-card mention\" title=\"Solidarity for Pigs\">neimzr4luzerz</a> @<a href=\"https://gs.smuglo.li/user/2326\" class=\"h-card mention\" title=\"Dolus_McHonest\">dolus</a> childhood poring over Strong's concordance and a koine Greek dictionary, fast forward to 2017 and some fuckstick who translates japanese jackoff material tells me you just need to make it sound right in English","context":"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26","conversation":"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26","id":"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment","inReplyTo":"tag:shitposter.club,2017-05-05:noticeId=2827849:objectType=comment","inReplyToStatusId":2827849,"published":"2017-05-05T08:51:48Z","sensitive":false,"summary":null,"tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Note"}
\ No newline at end of file
diff --git a/test/fixtures/tesla_mock/moonman@shitposter.club.json b/test/fixtures/tesla_mock/moonman@shitposter.club.json
new file mode 100644
index 000000000..8f9ced1dd
--- /dev/null
+++ b/test/fixtures/tesla_mock/moonman@shitposter.club.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://shitposter.club/schemas/litepub-0.1.jsonld",{"@language":"und"}],"attachment":[],"endpoints":{"oauthAuthorizationEndpoint":"https://shitposter.club/oauth/authorize","oauthRegistrationEndpoint":"https://shitposter.club/api/v1/apps","oauthTokenEndpoint":"https://shitposter.club/oauth/token","sharedInbox":"https://shitposter.club/inbox"},"followers":"https://shitposter.club/users/moonman/followers","following":"https://shitposter.club/users/moonman/following","icon":{"type":"Image","url":"https://shitposter.club/media/bda6e00074f6a02cbf32ddb0abec08151eb4c795e580927ff7ad638d00cde4c8.jpg?name=blob.jpg"},"id":"https://shitposter.club/users/moonman","image":{"type":"Image","url":"https://shitposter.club/media/4eefb90d-cdb2-2b4f-5f29-7612856a99d2/4eefb90d-cdb2-2b4f-5f29-7612856a99d2.jpeg"},"inbox":"https://shitposter.club/users/moonman/inbox","manuallyApprovesFollowers":false,"name":"Captain Howdy","outbox":"https://shitposter.club/users/moonman/outbox","preferredUsername":"moonman","publicKey":{"id":"https://shitposter.club/users/moonman#main-key","owner":"https://shitposter.club/users/moonman","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnOTitJ19ZqcOZHwSXQUM\nJq9ip4GNblp83LgwG1t5c2h2iaI3fXMsB4EaEBs8XHsoSFyDeDNRSPE3mtVgOnWv\n1eaXWMDerBT06th6DrElD9k5IoEPtZRY4HtZa1xGnte7+6RjuPOzZ1fR9C8WxGgi\nwb9iOUMhazpo85fC3iKCAL5XhiuA3Nas57MDJgueeI9BF+2oFelFZdMSWwG96uch\niDfp8nfpkmzYI6SWbylObjm8RsfZbGTosLHwWyJPEITeYI/5M0XwJe9dgVI1rVNU\n52kplWOGTo1rm6V0AMHaYAd9RpiXxe8xt5OeranrsE/5LvEQUl0fz7SE36YmsOaH\nTwIDAQAB\n-----END PUBLIC KEY-----\n\n"},"summary":"EMAIL:shitposterclub@gmail.com<br>XMPP: moon@talk.shitposter.club<br>PRONOUNS: none of your business<br><br>Purported leftist kike piece of shit","tag":[],"type":"Person","url":"https://shitposter.club/users/moonman"}
\ No newline at end of file
diff --git a/test/fixtures/tesla_mock/relay@mastdon.example.org.json b/test/fixtures/tesla_mock/relay@mastdon.example.org.json
new file mode 100644
index 000000000..c1fab7d3b
--- /dev/null
+++ b/test/fixtures/tesla_mock/relay@mastdon.example.org.json
@@ -0,0 +1,55 @@
+{
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "sensitive": "as:sensitive",
+ "movedTo": "as:movedTo",
+ "Hashtag": "as:Hashtag",
+ "ostatus": "http://ostatus.org#",
+ "atomUri": "ostatus:atomUri",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "toot": "http://joinmastodon.org/ns#",
+ "Emoji": "toot:Emoji"
+ }],
+ "id": "http://mastodon.example.org/users/admin",
+ "type": "Application",
+ "invisible": true,
+ "following": "http://mastodon.example.org/users/admin/following",
+ "followers": "http://mastodon.example.org/users/admin/followers",
+ "inbox": "http://mastodon.example.org/users/admin/inbox",
+ "outbox": "http://mastodon.example.org/users/admin/outbox",
+ "preferredUsername": "admin",
+ "name": null,
+ "summary": "\u003cp\u003e\u003c/p\u003e",
+ "url": "http://mastodon.example.org/@admin",
+ "manuallyApprovesFollowers": false,
+ "publicKey": {
+ "id": "http://mastodon.example.org/users/admin#main-key",
+ "owner": "http://mastodon.example.org/users/admin",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"
+ },
+ "attachment": [{
+ "type": "PropertyValue",
+ "name": "foo",
+ "value": "bar"
+ },
+ {
+ "type": "PropertyValue",
+ "name": "foo1",
+ "value": "bar1"
+ }
+ ],
+ "endpoints": {
+ "sharedInbox": "http://mastodon.example.org/inbox"
+ },
+ "icon": {
+ "type": "Image",
+ "mediaType": "image/jpeg",
+ "url": "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
+ },
+ "image": {
+ "type": "Image",
+ "mediaType": "image/png",
+ "url": "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
+ }
+}
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index 3bff51527..087bdbcc2 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -1,272 +1,272 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FormatterTest do
alias Pleroma.Formatter
alias Pleroma.User
use Pleroma.DataCase
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe ".add_hashtag_links" do
test "turns hashtags into links" do
text = "I love #cofe and #2hu"
expected_text =
~s(I love <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe" rel="tag ugc">#cofe</a> and <a class="hashtag" data-tag="2hu" href="http://localhost:4001/tag/2hu" rel="tag ugc">#2hu</a>)
assert {^expected_text, [], _tags} = Formatter.linkify(text)
end
test "does not turn html characters to tags" do
text = "#fact_3: pleroma does what mastodon't"
expected_text =
~s(<a class="hashtag" data-tag="fact_3" href="http://localhost:4001/tag/fact_3" rel="tag ugc">#fact_3</a>: pleroma does what mastodon't)
assert {^expected_text, [], _tags} = Formatter.linkify(text)
end
end
describe ".add_links" do
test "turning urls into links" do
text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ."
expected =
~S(Hey, check out <a href="https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla" rel="ugc">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> .)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social/@lambadalambda"
expected =
~S(<a href="https://mastodon.social/@lambadalambda" rel="ugc">https://mastodon.social/@lambadalambda</a>)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social:4000/@lambadalambda"
expected =
~S(<a href="https://mastodon.social:4000/@lambadalambda" rel="ugc">https://mastodon.social:4000/@lambadalambda</a>)
assert {^expected, [], []} = Formatter.linkify(text)
text = "@lambadalambda"
expected = "@lambadalambda"
assert {^expected, [], []} = Formatter.linkify(text)
text = "http://www.cs.vu.nl/~ast/intel/"
expected =
~S(<a href="http://www.cs.vu.nl/~ast/intel/" rel="ugc">http://www.cs.vu.nl/~ast/intel/</a>)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
expected =
"<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\" rel=\"ugc\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
expected =
"<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\" rel=\"ugc\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://www.google.co.jp/search?q=Nasim+Aghdam"
expected =
"<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\" rel=\"ugc\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Duff's_device"
expected =
"<a href=\"https://en.wikipedia.org/wiki/Duff's_device\" rel=\"ugc\">https://en.wikipedia.org/wiki/Duff's_device</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://pleroma.com https://pleroma.com/sucks"
expected =
"<a href=\"https://pleroma.com\" rel=\"ugc\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\" rel=\"ugc\">https://pleroma.com/sucks</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text = "xmpp:contact@hacktivis.me"
expected = "<a href=\"xmpp:contact@hacktivis.me\" rel=\"ugc\">xmpp:contact@hacktivis.me</a>"
assert {^expected, [], []} = Formatter.linkify(text)
text =
"magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com"
expected = "<a href=\"#{text}\" rel=\"ugc\">#{text}</a>"
assert {^expected, [], []} = Formatter.linkify(text)
end
end
describe "add_user_links" do
test "gives a replacement for user links, using local nicknames in user links text" do
text = "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme@archae.me"
gsimg = insert(:user, %{nickname: "gsimg"})
archaeme =
- insert(:user, %{
+ insert(:user,
nickname: "archa_eme_",
- info: %User.Info{source_data: %{"url" => "https://archeme/@archa_eme_"}}
- })
+ source_data: %{"url" => "https://archeme/@archa_eme_"}
+ )
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
{text, mentions, []} = Formatter.linkify(text)
assert length(mentions) == 3
expected_text =
~s(<span class="h-card"><a data-user="#{gsimg.id}" class="u-url mention" href="#{
gsimg.ap_id
}" rel="ugc">@<span>gsimg</span></a></span> According to <span class="h-card"><a data-user="#{
archaeme.id
}" class="u-url mention" href="#{"https://archeme/@archa_eme_"}" rel="ugc">@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class="h-card"><a data-user="#{
archaeme_remote.id
}" class="u-url mention" href="#{archaeme_remote.ap_id}" rel="ugc">@<span>archaeme</span></a></span>)
assert expected_text == text
end
test "gives a replacement for user links when the user is using Osada" do
{:ok, mike} = User.get_or_fetch("mike@osada.macgirvin.com")
text = "@mike@osada.macgirvin.com test"
{text, mentions, []} = Formatter.linkify(text)
assert length(mentions) == 1
expected_text =
~s(<span class="h-card"><a data-user="#{mike.id}" class="u-url mention" href="#{
mike.ap_id
}" rel="ugc">@<span>mike</span></a></span> test)
assert expected_text == text
end
test "gives a replacement for single-character local nicknames" do
text = "@o hi"
o = insert(:user, %{nickname: "o"})
{text, mentions, []} = Formatter.linkify(text)
assert length(mentions) == 1
expected_text =
~s(<span class="h-card"><a data-user="#{o.id}" class="u-url mention" href="#{o.ap_id}" rel="ugc">@<span>o</span></a></span> hi)
assert expected_text == text
end
test "does not give a replacement for single-character local nicknames who don't exist" do
text = "@a hi"
expected_text = "@a hi"
assert {^expected_text, [] = _mentions, [] = _tags} = Formatter.linkify(text)
end
test "given the 'safe_mention' option, it will only mention people in the beginning" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
text = " @#{user.nickname} @#{other_user.nickname} hey dudes i hate @#{third_user.nickname}"
{expected_text, mentions, [] = _tags} = Formatter.linkify(text, safe_mention: true)
assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}]
assert expected_text ==
~s(<span class="h-card"><a data-user="#{user.id}" class="u-url mention" href="#{
user.ap_id
}" rel="ugc">@<span>#{user.nickname}</span></a></span> <span class="h-card"><a data-user="#{
other_user.id
}" class="u-url mention" href="#{other_user.ap_id}" rel="ugc">@<span>#{
other_user.nickname
}</span></a></span> hey dudes i hate <span class="h-card"><a data-user="#{
third_user.id
}" class="u-url mention" href="#{third_user.ap_id}" rel="ugc">@<span>#{
third_user.nickname
}</span></a></span>)
end
test "given the 'safe_mention' option, it will still work without any mention" do
text = "A post without any mention"
{expected_text, mentions, [] = _tags} = Formatter.linkify(text, safe_mention: true)
assert mentions == []
assert expected_text == text
end
test "given the 'safe_mention' option, it will keep text after newlines" do
user = insert(:user)
text = " @#{user.nickname}\n hey dude\n\nhow are you doing?"
{expected_text, _, _} = Formatter.linkify(text, safe_mention: true)
assert expected_text =~ "how are you doing?"
end
test "it can parse mentions and return the relevant users" do
text =
"@@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me and @o and @@@jimm"
o = insert(:user, %{nickname: "o"})
jimm = insert(:user, %{nickname: "jimm"})
gsimg = insert(:user, %{nickname: "gsimg"})
archaeme = insert(:user, %{nickname: "archaeme"})
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
expected_mentions = [
{"@archaeme", archaeme},
{"@archaeme@archae.me", archaeme_remote},
{"@gsimg", gsimg},
{"@jimm", jimm},
{"@o", o}
]
assert {_text, ^expected_mentions, []} = Formatter.linkify(text)
end
end
describe ".parse_tags" do
test "parses tags in the text" do
text = "Here's a #Test. Maybe these are #working or not. What about #漢字? And #は。"
expected_tags = [
{"#Test", "test"},
{"#working", "working"},
{"#は", "は"},
{"#漢字", "漢字"}
]
assert {_text, [], ^expected_tags} = Formatter.linkify(text)
end
end
test "it escapes HTML in plain text" do
text = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1"
expected = "hello &amp; world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1"
assert Formatter.html_escape(text, "text/plain") == expected
end
end
diff --git a/test/marker_test.exs b/test/marker_test.exs
new file mode 100644
index 000000000..04bd67fe6
--- /dev/null
+++ b/test/marker_test.exs
@@ -0,0 +1,51 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MarkerTest do
+ use Pleroma.DataCase
+ alias Pleroma.Marker
+
+ import Pleroma.Factory
+
+ describe "get_markers/2" do
+ test "returns user markers" do
+ user = insert(:user)
+ marker = insert(:marker, user: user)
+ insert(:marker, timeline: "home", user: user)
+ assert Marker.get_markers(user, ["notifications"]) == [refresh_record(marker)]
+ end
+ end
+
+ describe "upsert/2" do
+ test "creates a marker" do
+ user = insert(:user)
+
+ {:ok, %{"notifications" => %Marker{} = marker}} =
+ Marker.upsert(
+ user,
+ %{"notifications" => %{"last_read_id" => "34"}}
+ )
+
+ assert marker.timeline == "notifications"
+ assert marker.last_read_id == "34"
+ assert marker.lock_version == 0
+ end
+
+ test "updates exist marker" do
+ user = insert(:user)
+ marker = insert(:marker, user: user, last_read_id: "8909")
+
+ {:ok, %{"notifications" => %Marker{}}} =
+ Marker.upsert(
+ user,
+ %{"notifications" => %{"last_read_id" => "9909"}}
+ )
+
+ marker = refresh_record(marker)
+ assert marker.timeline == "notifications"
+ assert marker.last_read_id == "9909"
+ assert marker.lock_version == 0
+ end
+ end
+end
diff --git a/test/moderation_log_test.exs b/test/moderation_log_test.exs
index a39a00e02..4240f6a65 100644
--- a/test/moderation_log_test.exs
+++ b/test/moderation_log_test.exs
@@ -1,297 +1,297 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ModerationLogTest do
alias Pleroma.Activity
alias Pleroma.ModerationLog
use Pleroma.DataCase
import Pleroma.Factory
describe "user moderation" do
setup do
- admin = insert(:user, info: %{is_admin: true})
- moderator = insert(:user, info: %{is_moderator: true})
+ admin = insert(:user, is_admin: true)
+ moderator = insert(:user, is_moderator: true)
subject1 = insert(:user)
subject2 = insert(:user)
[admin: admin, moderator: moderator, subject1: subject1, subject2: subject2]
end
test "logging user deletion by moderator", %{moderator: moderator, subject1: subject1} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
- subject: subject1,
+ subject: [subject1],
action: "delete"
})
log = Repo.one(ModerationLog)
- assert log.data["message"] == "@#{moderator.nickname} deleted user @#{subject1.nickname}"
+ assert log.data["message"] == "@#{moderator.nickname} deleted users: @#{subject1.nickname}"
end
test "logging user creation by moderator", %{
moderator: moderator,
subject1: subject1,
subject2: subject2
} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
subjects: [subject1, subject2],
action: "create"
})
log = Repo.one(ModerationLog)
assert log.data["message"] ==
"@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}"
end
test "logging user follow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: admin,
followed: subject1,
follower: subject2,
action: "follow"
})
log = Repo.one(ModerationLog)
assert log.data["message"] ==
"@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}"
end
test "logging user unfollow by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: admin,
followed: subject1,
follower: subject2,
action: "unfollow"
})
log = Repo.one(ModerationLog)
assert log.data["message"] ==
"@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}"
end
test "logging user tagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: admin,
nicknames: [subject1.nickname, subject2.nickname],
tags: ["foo", "bar"],
action: "tag"
})
log = Repo.one(ModerationLog)
users =
[subject1.nickname, subject2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["foo", "bar"] |> Enum.join(", ")
assert log.data["message"] == "@#{admin.nickname} added tags: #{tags} to users: #{users}"
end
test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: admin,
nicknames: [subject1.nickname, subject2.nickname],
tags: ["foo", "bar"],
action: "untag"
})
log = Repo.one(ModerationLog)
users =
[subject1.nickname, subject2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["foo", "bar"] |> Enum.join(", ")
assert log.data["message"] ==
"@#{admin.nickname} removed tags: #{tags} from users: #{users}"
end
test "logging user grant by moderator", %{moderator: moderator, subject1: subject1} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
- subject: subject1,
+ subject: [subject1],
action: "grant",
permission: "moderator"
})
log = Repo.one(ModerationLog)
assert log.data["message"] == "@#{moderator.nickname} made @#{subject1.nickname} moderator"
end
test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
- subject: subject1,
+ subject: [subject1],
action: "revoke",
permission: "moderator"
})
log = Repo.one(ModerationLog)
assert log.data["message"] ==
"@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}"
end
test "logging relay follow", %{moderator: moderator} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "relay_follow",
target: "https://example.org/relay"
})
log = Repo.one(ModerationLog)
assert log.data["message"] ==
"@#{moderator.nickname} followed relay: https://example.org/relay"
end
test "logging relay unfollow", %{moderator: moderator} do
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "relay_unfollow",
target: "https://example.org/relay"
})
log = Repo.one(ModerationLog)
assert log.data["message"] ==
"@#{moderator.nickname} unfollowed relay: https://example.org/relay"
end
test "logging report update", %{moderator: moderator} do
report = %Activity{
id: "9m9I1F4p8ftrTP6QTI",
data: %{
"type" => "Flag",
"state" => "resolved"
}
}
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "report_update",
subject: report
})
log = Repo.one(ModerationLog)
assert log.data["message"] ==
"@#{moderator.nickname} updated report ##{report.id} with 'resolved' state"
end
test "logging report response", %{moderator: moderator} do
report = %Activity{
id: "9m9I1F4p8ftrTP6QTI",
data: %{
"type" => "Note"
}
}
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "report_response",
subject: report,
text: "look at this"
})
log = Repo.one(ModerationLog)
assert log.data["message"] ==
"@#{moderator.nickname} responded with 'look at this' to report ##{report.id}"
end
test "logging status sensitivity update", %{moderator: moderator} do
note = insert(:note_activity)
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "status_update",
subject: note,
sensitive: "true",
visibility: nil
})
log = Repo.one(ModerationLog)
assert log.data["message"] ==
"@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'"
end
test "logging status visibility update", %{moderator: moderator} do
note = insert(:note_activity)
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "status_update",
subject: note,
sensitive: nil,
visibility: "private"
})
log = Repo.one(ModerationLog)
assert log.data["message"] ==
"@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'"
end
test "logging status sensitivity & visibility update", %{moderator: moderator} do
note = insert(:note_activity)
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "status_update",
subject: note,
sensitive: "true",
visibility: "private"
})
log = Repo.one(ModerationLog)
assert log.data["message"] ==
"@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'"
end
test "logging status deletion", %{moderator: moderator} do
note = insert(:note_activity)
{:ok, _} =
ModerationLog.insert_log(%{
actor: moderator,
action: "status_delete",
subject_id: note.id
})
log = Repo.one(ModerationLog)
assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}"
end
end
end
diff --git a/test/notification_test.exs b/test/notification_test.exs
index 54c0f9877..f8d429223 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -1,726 +1,726 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.NotificationTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Notification
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Streamer
describe "create_notifications" do
test "notifies someone when they are directly addressed" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname} and @#{third_user.nickname}"
})
{:ok, [notification, other_notification]} = Notification.create_notifications(activity)
notified_ids = Enum.sort([notification.user_id, other_notification.user_id])
assert notified_ids == [other_user.id, third_user.id]
assert notification.activity_id == activity.id
assert other_notification.activity_id == activity.id
end
test "it creates a notification for subscribed users" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [notification]} = Notification.create_notifications(status)
assert notification.user_id == subscriber.id
end
test "does not create a notification for subscribed users if status is a reply" do
user = insert(:user)
other_user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, other_user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
{:ok, _reply_activity} =
CommonAPI.post(other_user, %{
"status" => "test reply",
"in_reply_to_status_id" => activity.id
})
user_notifications = Notification.for_user(user)
assert length(user_notifications) == 1
subscriber_notifications = Notification.for_user(subscriber)
assert Enum.empty?(subscriber_notifications)
end
end
describe "create_notification" do
@tag needs_streamer: true
test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do
user = insert(:user)
task = Task.async(fn -> assert_receive {:text, _}, 4_000 end)
task_user_notification = Task.async(fn -> assert_receive {:text, _}, 4_000 end)
Streamer.add_socket("user", %{transport_pid: task.pid, assigns: %{user: user}})
Streamer.add_socket(
"user:notification",
%{transport_pid: task_user_notification.pid, assigns: %{user: user}}
)
activity = insert(:note_activity)
notify = Notification.create_notification(activity, user)
assert notify.user_id == user.id
Task.await(task)
Task.await(task_user_notification)
end
test "it creates a notification for user if the user blocks the activity author" do
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
user = insert(:user)
{:ok, user} = User.block(user, author)
assert Notification.create_notification(activity, user)
end
test "it creates a notificatin for the user if the user mutes the activity author" do
muter = insert(:user)
muted = insert(:user)
{:ok, _} = User.mute(muter, muted)
muter = Repo.get(User, muter.id)
{:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"})
assert Notification.create_notification(activity, muter)
end
test "notification created if user is muted without notifications" do
muter = insert(:user)
muted = insert(:user)
{:ok, muter} = User.mute(muter, muted, false)
{:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"})
assert Notification.create_notification(activity, muter)
end
test "it creates a notification for an activity from a muted thread" do
muter = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(muter, %{"status" => "hey"})
CommonAPI.add_mute(muter, activity)
{:ok, activity} =
CommonAPI.post(other_user, %{
"status" => "Hi @#{muter.nickname}",
"in_reply_to_status_id" => activity.id
})
assert Notification.create_notification(activity, muter)
end
test "it disables notifications from followers" do
follower = insert(:user)
- followed = insert(:user, info: %{notification_settings: %{"followers" => false}})
+ followed = insert(:user, notification_settings: %{"followers" => false})
User.follow(follower, followed)
{:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed)
end
test "it disables notifications from non-followers" do
follower = insert(:user)
- followed = insert(:user, info: %{notification_settings: %{"non_followers" => false}})
+ followed = insert(:user, notification_settings: %{"non_followers" => false})
{:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed)
end
test "it disables notifications from people the user follows" do
- follower = insert(:user, info: %{notification_settings: %{"follows" => false}})
+ follower = insert(:user, notification_settings: %{"follows" => false})
followed = insert(:user)
User.follow(follower, followed)
follower = Repo.get(User, follower.id)
{:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"})
refute Notification.create_notification(activity, follower)
end
test "it disables notifications from people the user does not follow" do
- follower = insert(:user, info: %{notification_settings: %{"non_follows" => false}})
+ follower = insert(:user, notification_settings: %{"non_follows" => false})
followed = insert(:user)
{:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"})
refute Notification.create_notification(activity, follower)
end
test "it doesn't create a notification for user if he is the activity author" do
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
refute Notification.create_notification(activity, author)
end
test "it doesn't create a notification for follow-unfollow-follow chains" do
user = insert(:user)
followed_user = insert(:user)
{:ok, _, _, activity} = CommonAPI.follow(user, followed_user)
Notification.create_notification(activity, followed_user)
CommonAPI.unfollow(user, followed_user)
{:ok, _, _, activity_dupe} = CommonAPI.follow(user, followed_user)
refute Notification.create_notification(activity_dupe, followed_user)
end
test "it doesn't create duplicate notifications for follow+subscribed users" do
user = insert(:user)
subscriber = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(subscriber, user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [_notif]} = Notification.create_notifications(status)
end
test "it doesn't create subscription notifications if the recipient cannot see the status" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "inwisible", "visibility" => "direct"})
assert {:ok, []} == Notification.create_notifications(status)
end
end
describe "get notification" do
test "it gets a notification that belongs to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.get(other_user, notification.id)
assert notification.user_id == other_user.id
end
test "it returns error if the notification doesn't belong to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.get(user, notification.id)
end
end
describe "dismiss notification" do
test "it dismisses a notification that belongs to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.dismiss(other_user, notification.id)
assert notification.user_id == other_user.id
end
test "it returns error if the notification doesn't belong to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.dismiss(user, notification.id)
end
end
describe "clear notification" do
test "it clears all notifications belonging to the user" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname} and @#{third_user.nickname} !"
})
{:ok, _notifs} = Notification.create_notifications(activity)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "hey again @#{other_user.nickname} and @#{third_user.nickname} !"
})
{:ok, _notifs} = Notification.create_notifications(activity)
Notification.clear(other_user)
assert Notification.for_user(other_user) == []
assert Notification.for_user(third_user) != []
end
end
describe "set_read_up_to()" do
test "it sets all notifications as read up to a specified notification ID" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname}!"
})
{:ok, _activity} =
CommonAPI.post(user, %{
"status" => "hey again @#{other_user.nickname}!"
})
[n2, n1] = notifs = Notification.for_user(other_user)
assert length(notifs) == 2
assert n2.id > n1.id
{:ok, _activity} =
CommonAPI.post(user, %{
"status" => "hey yet again @#{other_user.nickname}!"
})
Notification.set_read_up_to(other_user, n2.id)
[n3, n2, n1] = Notification.for_user(other_user)
assert n1.seen == true
assert n2.seen == true
assert n3.seen == false
end
end
describe "for_user_since/2" do
defp days_ago(days) do
NaiveDateTime.add(
NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
-days * 60 * 60 * 24,
:second
)
end
test "Returns recent notifications" do
user1 = insert(:user)
user2 = insert(:user)
Enum.each(0..10, fn i ->
{:ok, _activity} =
CommonAPI.post(user1, %{
"status" => "hey ##{i} @#{user2.nickname}!"
})
end)
{old, new} = Enum.split(Notification.for_user(user2), 5)
Enum.each(old, fn notification ->
notification
|> cast(%{updated_at: days_ago(10)}, [:updated_at])
|> Pleroma.Repo.update!()
end)
recent_notifications_ids =
user2
|> Notification.for_user_since(
NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86_400, :second)
)
|> Enum.map(& &1.id)
Enum.each(old, fn %{id: id} ->
refute id in recent_notifications_ids
end)
Enum.each(new, fn %{id: id} ->
assert id in recent_notifications_ids
end)
end
end
describe "notification target determination" do
test "it sends notifications to addressed users in new messages" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname}!"
})
assert other_user in Notification.get_notified_from_activity(activity)
end
test "it sends notifications to mentioned users in new messages" do
user = insert(:user)
other_user = insert(:user)
create_activity = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"actor" => user.ap_id,
"object" => %{
"type" => "Note",
"content" => "message with a Mention tag, but no explicit tagging",
"tag" => [
%{
"type" => "Mention",
"href" => other_user.ap_id,
"name" => other_user.nickname
}
],
"attributedTo" => user.ap_id
}
}
{:ok, activity} = Transmogrifier.handle_incoming(create_activity)
assert other_user in Notification.get_notified_from_activity(activity)
end
test "it does not send notifications to users who are only cc in new messages" do
user = insert(:user)
other_user = insert(:user)
create_activity = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [other_user.ap_id],
"actor" => user.ap_id,
"object" => %{
"type" => "Note",
"content" => "hi everyone",
"attributedTo" => user.ap_id
}
}
{:ok, activity} = Transmogrifier.handle_incoming(create_activity)
assert other_user not in Notification.get_notified_from_activity(activity)
end
test "it does not send notification to mentioned users in likes" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity_one} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname}!"
})
{:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, third_user)
assert other_user not in Notification.get_notified_from_activity(activity_two)
end
test "it does not send notification to mentioned users in announces" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity_one} =
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname}!"
})
{:ok, activity_two, _} = CommonAPI.repeat(activity_one.id, third_user)
assert other_user not in Notification.get_notified_from_activity(activity_two)
end
end
describe "notification lifecycle" do
test "liking an activity results in 1 notification, then 0 if the activity is deleted" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
assert length(Notification.for_user(user)) == 1
{:ok, _} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
end
test "liking an activity results in 1 notification, then 0 if the activity is unliked" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
assert length(Notification.for_user(user)) == 1
{:ok, _, _, _} = CommonAPI.unfavorite(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
test "repeating an activity results in 1 notification, then 0 if the activity is deleted" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
assert length(Notification.for_user(user)) == 1
{:ok, _} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
end
test "repeating an activity results in 1 notification, then 0 if the activity is unrepeated" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
assert length(Notification.for_user(user)) == 1
{:ok, _, _} = CommonAPI.unrepeat(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
test "liking an activity which is already deleted does not generate a notification" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _deletion_activity} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
{:error, _} = CommonAPI.favorite(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
test "repeating an activity which is already deleted does not generate a notification" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _deletion_activity} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
{:error, _} = CommonAPI.repeat(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
test "replying to a deleted post without tagging does not generate a notification" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
{:ok, _deletion_activity} = CommonAPI.delete(activity.id, user)
{:ok, _reply_activity} =
CommonAPI.post(other_user, %{
"status" => "test reply",
"in_reply_to_status_id" => activity.id
})
assert Enum.empty?(Notification.for_user(user))
end
test "notifications are deleted if a local user is deleted" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}", "visibility" => "direct"})
refute Enum.empty?(Notification.for_user(other_user))
{:ok, job} = User.delete(user)
ObanHelpers.perform(job)
assert Enum.empty?(Notification.for_user(other_user))
end
test "notifications are deleted if a remote user is deleted" do
remote_user = insert(:user)
local_user = insert(:user)
dm_message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"actor" => remote_user.ap_id,
"id" => remote_user.ap_id <> "/activities/test",
"to" => [local_user.ap_id],
"cc" => [],
"object" => %{
"type" => "Note",
"content" => "Hello!",
"tag" => [
%{
"type" => "Mention",
"href" => local_user.ap_id,
"name" => "@#{local_user.nickname}"
}
],
"to" => [local_user.ap_id],
"cc" => [],
"attributedTo" => remote_user.ap_id
}
}
{:ok, _dm_activity} = Transmogrifier.handle_incoming(dm_message)
refute Enum.empty?(Notification.for_user(local_user))
delete_user_message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => remote_user.ap_id <> "/activities/delete",
"actor" => remote_user.ap_id,
"type" => "Delete",
"object" => remote_user.ap_id
}
{:ok, _delete_activity} = Transmogrifier.handle_incoming(delete_user_message)
ObanHelpers.perform_all()
assert Enum.empty?(Notification.for_user(local_user))
end
end
describe "for_user" do
test "it returns notifications for muted user without notifications" do
user = insert(:user)
muted = insert(:user)
{:ok, user} = User.mute(user, muted, false)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user)) == 1
end
test "it doesn't return notifications for muted user with notifications" do
user = insert(:user)
muted = insert(:user)
{:ok, user} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
test "it doesn't return notifications for blocked user" do
user = insert(:user)
blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
test "it doesn't return notificatitons for blocked domain" do
user = insert(:user)
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
test "it doesn't return notifications for muted thread" do
user = insert(:user)
another_user = insert(:user)
{:ok, activity} = CommonAPI.post(another_user, %{"status" => "hey @#{user.nickname}"})
{:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
assert Notification.for_user(user) == []
end
- test "it returns notifications for muted user with notifications and with_muted parameter" do
+ test "it returns notifications from a muted user when with_muted is set" do
user = insert(:user)
muted = insert(:user)
{:ok, user} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
- test "it returns notifications for blocked user and with_muted parameter" do
+ test "it doesn't return notifications from a blocked user when with_muted is set" do
user = insert(:user)
blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
- assert length(Notification.for_user(user, %{with_muted: true})) == 1
+ assert length(Notification.for_user(user, %{with_muted: true})) == 0
end
- test "it returns notificatitons for blocked domain and with_muted parameter" do
+ test "it doesn't return notifications from a domain-blocked user when with_muted is set" do
user = insert(:user)
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
- assert length(Notification.for_user(user, %{with_muted: true})) == 1
+ assert length(Notification.for_user(user, %{with_muted: true})) == 0
end
- test "it returns notifications for muted thread with_muted parameter" do
+ test "it returns notifications from muted threads when with_muted is set" do
user = insert(:user)
another_user = insert(:user)
{:ok, activity} = CommonAPI.post(another_user, %{"status" => "hey @#{user.nickname}"})
{:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
end
end
diff --git a/test/object/containment_test.exs b/test/object/containment_test.exs
index 61cd1b412..0dc2728b9 100644
--- a/test/object/containment_test.exs
+++ b/test/object/containment_test.exs
@@ -1,101 +1,101 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.ContainmentTest do
use Pleroma.DataCase
alias Pleroma.Object.Containment
alias Pleroma.User
import Pleroma.Factory
import ExUnit.CaptureLog
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "general origin containment" do
test "contain_origin_from_id() catches obvious spoofing attempts" do
data = %{
"id" => "http://example.com/~alyssa/activities/1234.json"
}
:error =
Containment.contain_origin_from_id(
"http://example.org/~alyssa/activities/1234.json",
data
)
end
test "contain_origin_from_id() allows alternate IDs within the same origin domain" do
data = %{
"id" => "http://example.com/~alyssa/activities/1234.json"
}
:ok =
Containment.contain_origin_from_id(
"http://example.com/~alyssa/activities/1234",
data
)
end
test "contain_origin_from_id() allows matching IDs" do
data = %{
"id" => "http://example.com/~alyssa/activities/1234.json"
}
:ok =
Containment.contain_origin_from_id(
"http://example.com/~alyssa/activities/1234.json",
data
)
end
test "users cannot be collided through fake direction spoofing attempts" do
_user =
insert(:user, %{
nickname: "rye@niu.moe",
local: false,
ap_id: "https://niu.moe/users/rye",
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
})
assert capture_log(fn ->
{:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye")
end) =~
- "[error] Could not decode user at fetch https://n1u.moe/users/rye, {:error, :error}"
+ "[error] Could not decode user at fetch https://n1u.moe/users/rye"
end
end
describe "containment of children" do
test "contain_child() catches spoofing attempts" do
data = %{
"id" => "http://example.com/whatever",
"type" => "Create",
"object" => %{
"id" => "http://example.net/~alyssa/activities/1234",
"attributedTo" => "http://example.org/~alyssa"
},
"actor" => "http://example.com/~bob"
}
:error = Containment.contain_child(data)
end
test "contain_child() allows correct origins" do
data = %{
"id" => "http://example.org/~alyssa/activities/5678",
"type" => "Create",
"object" => %{
"id" => "http://example.org/~alyssa/activities/1234",
"attributedTo" => "http://example.org/~alyssa"
},
"actor" => "http://example.org/~alyssa"
}
:ok = Containment.contain_child(data)
end
end
end
diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs
index 895a73d2c..9ae6b015d 100644
--- a/test/object/fetcher_test.exs
+++ b/test/object/fetcher_test.exs
@@ -1,186 +1,153 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.FetcherTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
import Tesla.Mock
import Mock
setup do
mock(fn
%{method: :get, url: "https://mastodon.example.org/users/userisgone"} ->
%Tesla.Env{status: 410}
%{method: :get, url: "https://mastodon.example.org/users/userisgone404"} ->
%Tesla.Env{status: 404}
env ->
apply(HttpRequestMock, :request, [env])
end)
:ok
end
describe "actor origin containment" do
- test_with_mock "it rejects objects with a bogus origin",
- Pleroma.Web.OStatus,
- [:passthrough],
- [] do
+ test "it rejects objects with a bogus origin" do
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
-
- refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
end
- test_with_mock "it rejects objects when attributedTo is wrong (variant 1)",
- Pleroma.Web.OStatus,
- [:passthrough],
- [] do
+ test "it rejects objects when attributedTo is wrong (variant 1)" do
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
-
- refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
end
- test_with_mock "it rejects objects when attributedTo is wrong (variant 2)",
- Pleroma.Web.OStatus,
- [:passthrough],
- [] do
+ test "it rejects objects when attributedTo is wrong (variant 2)" do
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
-
- refute called(Pleroma.Web.OStatus.fetch_activity_from_url(:_))
end
end
describe "fetching an object" do
test "it fetches an object" do
{:ok, object} =
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
assert activity = Activity.get_create_by_object_ap_id(object.data["id"])
assert activity.data["id"]
{:ok, object_again} =
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
assert [attachment] = object.data["attachment"]
assert is_list(attachment["url"])
assert object == object_again
end
-
- test "it works with objects only available via Ostatus" do
- {:ok, object} = Fetcher.fetch_object_from_id("https://shitposter.club/notice/2827873")
- assert activity = Activity.get_create_by_object_ap_id(object.data["id"])
- assert activity.data["id"]
-
- {:ok, object_again} = Fetcher.fetch_object_from_id("https://shitposter.club/notice/2827873")
-
- assert object == object_again
- end
-
- test "it correctly stitches up conversations between ostatus and ap" do
- last = "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
- {:ok, object} = Fetcher.fetch_object_from_id(last)
-
- object = Object.get_by_ap_id(object.data["inReplyTo"])
- assert object
- end
end
describe "implementation quirks" do
test "it can fetch plume articles" do
{:ok, object} =
Fetcher.fetch_object_from_id(
"https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"
)
assert object
end
test "it can fetch peertube videos" do
{:ok, object} =
Fetcher.fetch_object_from_id(
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
)
assert object
end
test "it can fetch wedistribute articles" do
{:ok, object} =
Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810")
assert object
end
test "all objects with fake directions are rejected by the object fetcher" do
assert {:error, _} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://info.pleroma.site/activity4.json"
)
end
test "handle HTTP 410 Gone response" do
assert {:error, "Object has been deleted"} ==
Fetcher.fetch_and_contain_remote_object_from_id(
"https://mastodon.example.org/users/userisgone"
)
end
test "handle HTTP 404 response" do
assert {:error, "Object has been deleted"} ==
Fetcher.fetch_and_contain_remote_object_from_id(
"https://mastodon.example.org/users/userisgone404"
)
end
end
describe "pruning" do
test "it can refetch pruned objects" do
object_id = "http://mastodon.example.org/@admin/99541947525187367"
{:ok, object} = Fetcher.fetch_object_from_id(object_id)
assert object
{:ok, _object} = Object.prune(object)
refute Object.get_by_ap_id(object_id)
{:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id)
assert object.data["id"] == object_two.data["id"]
assert object.id != object_two.id
end
end
describe "signed fetches" do
clear_config([:activitypub, :sign_object_fetches])
test_with_mock "it signs fetches when configured to do so",
Pleroma.Signature,
[:passthrough],
[] do
Pleroma.Config.put([:activitypub, :sign_object_fetches], true)
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
assert called(Pleroma.Signature.sign(:_, :_))
end
test_with_mock "it doesn't sign fetches when not configured to do so",
Pleroma.Signature,
[:passthrough],
[] do
Pleroma.Config.put([:activitypub, :sign_object_fetches], false)
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
refute called(Pleroma.Signature.sign(:_, :_))
end
end
end
diff --git a/test/plugs/admin_secret_authentication_plug_test.exs b/test/plugs/admin_secret_authentication_plug_test.exs
index e1d4b391f..c94a62c10 100644
--- a/test/plugs/admin_secret_authentication_plug_test.exs
+++ b/test/plugs/admin_secret_authentication_plug_test.exs
@@ -1,42 +1,42 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AdminSecretAuthenticationPlugTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
alias Pleroma.Plugs.AdminSecretAuthenticationPlug
test "does nothing if a user is assigned", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
ret_conn =
conn
|> AdminSecretAuthenticationPlug.call(%{})
assert conn == ret_conn
end
test "with secret set and given in the 'admin_token' parameter, it assigns an admin user", %{
conn: conn
} do
Pleroma.Config.put(:admin_token, "password123")
conn =
%{conn | params: %{"admin_token" => "wrong_password"}}
|> AdminSecretAuthenticationPlug.call(%{})
refute conn.assigns[:user]
conn =
%{conn | params: %{"admin_token" => "password123"}}
|> AdminSecretAuthenticationPlug.call(%{})
- assert conn.assigns[:user].info.is_admin
+ assert conn.assigns[:user].is_admin
end
end
diff --git a/test/plugs/user_enabled_plug_test.exs b/test/plugs/user_enabled_plug_test.exs
index c0fafcab1..996a7d77b 100644
--- a/test/plugs/user_enabled_plug_test.exs
+++ b/test/plugs/user_enabled_plug_test.exs
@@ -1,43 +1,43 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserEnabledPlugTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Plugs.UserEnabledPlug
import Pleroma.Factory
test "doesn't do anything if the user isn't set", %{conn: conn} do
ret_conn =
conn
|> UserEnabledPlug.call(%{})
assert ret_conn == conn
end
test "with a user that is deactivated, it removes that user", %{conn: conn} do
- user = insert(:user, info: %{deactivated: true})
+ user = insert(:user, deactivated: true)
conn =
conn
|> assign(:user, user)
|> UserEnabledPlug.call(%{})
assert conn.assigns.user == nil
end
test "with a user that is not deactivated, it does nothing", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
ret_conn =
conn
|> UserEnabledPlug.call(%{})
assert conn == ret_conn
end
end
diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs
index 9e05fff18..136dcc54e 100644
--- a/test/plugs/user_is_admin_plug_test.exs
+++ b/test/plugs/user_is_admin_plug_test.exs
@@ -1,43 +1,43 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserIsAdminPlugTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Plugs.UserIsAdminPlug
import Pleroma.Factory
test "accepts a user that is admin" do
- user = insert(:user, info: %{is_admin: true})
+ user = insert(:user, is_admin: true)
conn =
build_conn()
|> assign(:user, user)
ret_conn =
conn
|> UserIsAdminPlug.call(%{})
assert conn == ret_conn
end
test "denies a user that isn't admin" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
|> UserIsAdminPlug.call(%{})
assert conn.status == 403
end
test "denies when a user isn't set" do
conn =
build_conn()
|> UserIsAdminPlug.call(%{})
assert conn.status == 403
end
end
diff --git a/test/safe_jsonb_set_test.exs b/test/safe_jsonb_set_test.exs
new file mode 100644
index 000000000..748540570
--- /dev/null
+++ b/test/safe_jsonb_set_test.exs
@@ -0,0 +1,12 @@
+defmodule Pleroma.SafeJsonbSetTest do
+ use Pleroma.DataCase
+
+ test "it doesn't wipe the object when asked to set the value to NULL" do
+ assert %{rows: [[%{"key" => "value", "test" => nil}]]} =
+ Ecto.Adapters.SQL.query!(
+ Pleroma.Repo,
+ "select safe_jsonb_set('{\"key\": \"value\"}'::jsonb, '{test}', NULL);",
+ []
+ )
+ end
+end
diff --git a/test/signature_test.exs b/test/signature_test.exs
index 96c8ba07a..15cf10fb6 100644
--- a/test/signature_test.exs
+++ b/test/signature_test.exs
@@ -1,130 +1,129 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.SignatureTest do
use Pleroma.DataCase
import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
import Mock
alias Pleroma.Signature
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
@private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA48qb4v6kqigZutO9Ot0wkp27GIF2LiVaADgxQORZozZR63jH\nTaoOrS3Xhngbgc8SSOhfXET3omzeCLqaLNfXnZ8OXmuhJfJSU6mPUvmZ9QdT332j\nfN/g3iWGhYMf/M9ftCKh96nvFVO/tMruzS9xx7tkrfJjehdxh/3LlJMMImPtwcD7\nkFXwyt1qZTAU6Si4oQAJxRDQXHp1ttLl3Ob829VM7IKkrVmY8TD+JSlV0jtVJPj6\n1J19ytKTx/7UaucYvb9HIiBpkuiy5n/irDqKLVf5QEdZoNCdojOZlKJmTLqHhzKP\n3E9TxsUjhrf4/EqegNc/j982RvOxeu4i40zMQwIDAQABAoIBAQDH5DXjfh21i7b4\ncXJuw0cqget617CDUhemdakTDs9yH+rHPZd3mbGDWuT0hVVuFe4vuGpmJ8c+61X0\nRvugOlBlavxK8xvYlsqTzAmPgKUPljyNtEzQ+gz0I+3mH2jkin2rL3D+SksZZgKm\nfiYMPIQWB2WUF04gB46DDb2mRVuymGHyBOQjIx3WC0KW2mzfoFUFRlZEF+Nt8Ilw\nT+g/u0aZ1IWoszbsVFOEdghgZET0HEarum0B2Je/ozcPYtwmU10iBANGMKdLqaP/\nj954BPunrUf6gmlnLZKIKklJj0advx0NA+cL79+zeVB3zexRYSA5o9q0WPhiuTwR\n/aedWHnBAoGBAP0sDWBAM1Y4TRAf8ZI9PcztwLyHPzfEIqzbObJJnx1icUMt7BWi\n+/RMOnhrlPGE1kMhOqSxvXYN3u+eSmWTqai2sSH5Hdw2EqnrISSTnwNUPINX7fHH\njEkgmXQ6ixE48SuBZnb4w1EjdB/BA6/sjL+FNhggOc87tizLTkMXmMtTAoGBAOZV\n+wPuAMBDBXmbmxCuDIjoVmgSlgeRunB1SA8RCPAFAiUo3+/zEgzW2Oz8kgI+xVwM\n33XkLKrWG1Orhpp6Hm57MjIc5MG+zF4/YRDpE/KNG9qU1tiz0UD5hOpIU9pP4bR/\ngxgPxZzvbk4h5BfHWLpjlk8UUpgk6uxqfti48c1RAoGBALBOKDZ6HwYRCSGMjUcg\n3NPEUi84JD8qmFc2B7Tv7h2he2ykIz9iFAGpwCIyETQsJKX1Ewi0OlNnD3RhEEAy\nl7jFGQ+mkzPSeCbadmcpYlgIJmf1KN/x7fDTAepeBpCEzfZVE80QKbxsaybd3Dp8\nCfwpwWUFtBxr4c7J+gNhAGe/AoGAPn8ZyqkrPv9wXtyfqFjxQbx4pWhVmNwrkBPi\nZ2Qh3q4dNOPwTvTO8vjghvzIyR8rAZzkjOJKVFgftgYWUZfM5gE7T2mTkBYq8W+U\n8LetF+S9qAM2gDnaDx0kuUTCq7t87DKk6URuQ/SbI0wCzYjjRD99KxvChVGPBHKo\n1DjqMuECgYEAgJGNm7/lJCS2wk81whfy/ttKGsEIkyhPFYQmdGzSYC5aDc2gp1R3\nxtOkYEvdjfaLfDGEa4UX8CHHF+w3t9u8hBtcdhMH6GYb9iv6z0VBTt4A/11HUR49\n3Z7TQ18Iyh3jAUCzFV9IJlLIExq5Y7P4B3ojWFBN607sDCt8BMPbDYs=\n-----END RSA PRIVATE KEY-----"
@public_key %{
"id" => "https://mastodon.social/users/lambadalambda#main-key",
"owner" => "https://mastodon.social/users/lambadalambda",
"publicKeyPem" =>
"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0P/Tq4gb4G/QVuMGbJo\nC/AfMNcv+m7NfrlOwkVzcU47jgESuYI4UtJayissCdBycHUnfVUd9qol+eznSODz\nCJhfJloqEIC+aSnuEPGA0POtWad6DU0E6/Ho5zQn5WAWUwbRQqowbrsm/GHo2+3v\neR5jGenwA6sYhINg/c3QQbksyV0uJ20Umyx88w8+TJuv53twOfmyDWuYNoQ3y5cc\nHKOZcLHxYOhvwg3PFaGfFHMFiNmF40dTXt9K96r7sbzc44iLD+VphbMPJEjkMuf8\nPGEFOBzy8pm3wJZw2v32RNW2VESwMYyqDzwHXGSq1a73cS7hEnc79gXlELsK04L9\nQQIDAQAB\n-----END PUBLIC KEY-----\n"
}
@rsa_public_key {
:RSAPublicKey,
24_650_000_183_914_698_290_885_268_529_673_621_967_457_234_469_123_179_408_466_269_598_577_505_928_170_923_974_132_111_403_341_217_239_999_189_084_572_368_839_502_170_501_850_920_051_662_384_964_248_315_257_926_552_945_648_828_895_432_624_227_029_881_278_113_244_073_644_360_744_504_606_177_648_469_825_063_267_913_017_309_199_785_535_546_734_904_379_798_564_556_494_962_268_682_532_371_146_333_972_821_570_577_277_375_020_977_087_539_994_500_097_107_935_618_711_808_260_846_821_077_839_605_098_669_707_417_692_791_905_543_116_911_754_774_323_678_879_466_618_738_207_538_013_885_607_095_203_516_030_057_611_111_308_904_599_045_146_148_350_745_339_208_006_497_478_057_622_336_882_506_112_530_056_970_653_403_292_123_624_453_213_574_011_183_684_739_084_105_206_483_178_943_532_208_537_215_396_831_110_268_758_639_826_369_857,
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
65_537
}
defp make_fake_signature(key_id), do: "keyId=\"#{key_id}\""
defp make_fake_conn(key_id),
do: %Plug.Conn{req_headers: %{"signature" => make_fake_signature(key_id <> "#main-key")}}
describe "fetch_public_key/1" do
test "it returns key" do
expected_result = {:ok, @rsa_public_key}
- user = insert(:user, %{info: %{source_data: %{"publicKey" => @public_key}}})
+ user = insert(:user, source_data: %{"publicKey" => @public_key})
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == expected_result
end
test "it returns error when not found user" do
assert capture_log(fn ->
assert Signature.fetch_public_key(make_fake_conn("test-ap_id")) == {:error, :error}
end) =~ "[error] Could not decode user"
end
test "it returns error if public key is empty" do
- user = insert(:user, %{info: %{source_data: %{"publicKey" => %{}}}})
+ user = insert(:user, source_data: %{"publicKey" => %{}})
assert Signature.fetch_public_key(make_fake_conn(user.ap_id)) == {:error, :error}
end
end
describe "refetch_public_key/1" do
test "it returns key" do
ap_id = "https://mastodon.social/users/lambadalambda"
assert Signature.refetch_public_key(make_fake_conn(ap_id)) == {:ok, @rsa_public_key}
end
test "it returns error when not found user" do
assert capture_log(fn ->
- assert Signature.refetch_public_key(make_fake_conn("test-ap_id")) ==
- {:error, {:error, :ok}}
+ {:error, _} = Signature.refetch_public_key(make_fake_conn("test-ap_id"))
end) =~ "[error] Could not decode user"
end
end
describe "sign/2" do
test "it returns signature headers" do
user =
insert(:user, %{
ap_id: "https://mastodon.social/users/lambadalambda",
keys: @private_key
})
assert Signature.sign(
user,
%{
host: "test.test",
"content-length": 100
}
) ==
"keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\""
end
test "it returns error" do
user = insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", keys: ""})
assert Signature.sign(
user,
%{host: "test.test", "content-length": 100}
) == {:error, []}
end
end
describe "key_id_to_actor_id/1" do
test "it properly deduces the actor id for misskey" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
"https://example.com/users/1234"
end
test "it properly deduces the actor id for mastodon and pleroma" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234#main-key") ==
"https://example.com/users/1234"
end
end
describe "signed_date" do
test "it returns formatted current date" do
with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do
assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT"
end
end
test "it returns formatted date" do
assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) ==
"Fri, 23 Aug 2019 08:11:24 GMT"
end
end
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index b180844cd..41e2b8004 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -1,400 +1,389 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Factory do
use ExMachina.Ecto, repo: Pleroma.Repo
alias Pleroma.Object
alias Pleroma.User
def participation_factory do
conversation = insert(:conversation)
user = insert(:user)
%Pleroma.Conversation.Participation{
conversation: conversation,
user: user,
read: false
}
end
def conversation_factory do
%Pleroma.Conversation{
ap_id: sequence(:ap_id, &"https://some_conversation/#{&1}")
}
end
def user_factory do
user = %User{
name: sequence(:name, &"Test テスト User #{&1}"),
email: sequence(:email, &"user#{&1}@example.com"),
nickname: sequence(:nickname, &"nick#{&1}"),
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"),
info: %{},
last_digest_emailed_at: NaiveDateTime.utc_now()
}
%{
user
| ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user),
following: [User.ap_id(user)]
}
end
def note_factory(attrs \\ %{}) do
text = sequence(:text, &"This is :moominmamma: note #{&1}")
user = attrs[:user] || insert(:user)
data = %{
"type" => "Note",
"content" => text,
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"actor" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"likes" => [],
"like_count" => 0,
"context" => "2hu",
"summary" => "2hu",
"tag" => ["2hu"],
"emoji" => %{
"2hu" => "corndog.png"
}
}
%Pleroma.Object{
data: merge_attributes(data, Map.get(attrs, :data, %{}))
}
end
def audio_factory(attrs \\ %{}) do
text = sequence(:text, &"lain radio episode #{&1}")
user = attrs[:user] || insert(:user)
data = %{
"type" => "Audio",
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"artist" => "lain",
"title" => text,
"album" => "lain radio",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"actor" => user.ap_id,
"length" => 180_000
}
%Pleroma.Object{
data: merge_attributes(data, Map.get(attrs, :data, %{}))
}
end
def listen_factory do
audio = insert(:audio)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Listen",
"actor" => audio.data["actor"],
"to" => audio.data["to"],
"object" => audio.data,
"published" => audio.data["published"]
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
end
def direct_note_factory do
user2 = insert(:user)
%Pleroma.Object{data: data} = note_factory()
%Pleroma.Object{data: Map.merge(data, %{"to" => [user2.ap_id]})}
end
def article_factory do
note_factory()
|> Map.put("type", "Article")
end
def tombstone_factory do
data = %{
"type" => "Tombstone",
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"formerType" => "Note",
"deleted" => DateTime.utc_now() |> DateTime.to_iso8601()
}
%Pleroma.Object{
data: data
}
end
def direct_note_activity_factory do
dm = insert(:direct_note)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => dm.data["actor"],
"to" => dm.data["to"],
"object" => dm.data,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => dm.data["context"]
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
end
def note_activity_factory(attrs \\ %{}) do
user = attrs[:user] || insert(:user)
note = attrs[:note] || insert(:note, user: user)
data_attrs = attrs[:data_attrs] || %{}
attrs = Map.drop(attrs, [:user, :note, :data_attrs])
data =
%{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => note.data["actor"],
"to" => note.data["to"],
"object" => note.data["id"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => note.data["context"]
}
|> Map.merge(data_attrs)
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
|> Map.merge(attrs)
end
defp expiration_offset_by_minutes(attrs, minutes) do
scheduled_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(minutes), :millisecond)
|> NaiveDateTime.truncate(:second)
%Pleroma.ActivityExpiration{}
|> Map.merge(attrs)
|> Map.put(:scheduled_at, scheduled_at)
end
def expiration_in_the_past_factory(attrs \\ %{}) do
expiration_offset_by_minutes(attrs, -60)
end
def expiration_in_the_future_factory(attrs \\ %{}) do
expiration_offset_by_minutes(attrs, 61)
end
def article_activity_factory do
article = insert(:article)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => article.data["actor"],
"to" => article.data["to"],
"object" => article.data,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => article.data["context"]
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
end
def announce_activity_factory(attrs \\ %{}) do
note_activity = attrs[:note_activity] || insert(:note_activity)
user = attrs[:user] || insert(:user)
data = %{
"type" => "Announce",
"actor" => note_activity.actor,
"object" => note_activity.data["id"],
"to" => [user.follower_address, note_activity.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => note_activity.data["context"]
}
%Pleroma.Activity{
data: data,
actor: user.ap_id,
recipients: data["to"]
}
end
def like_activity_factory(attrs \\ %{}) do
note_activity = attrs[:note_activity] || insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
data =
%{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"actor" => user.ap_id,
"type" => "Like",
"object" => object.data["id"],
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
|> Map.merge(attrs[:data_attrs] || %{})
%Pleroma.Activity{
data: data
}
end
def follow_activity_factory do
follower = insert(:user)
followed = insert(:user)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"actor" => follower.ap_id,
"type" => "Follow",
"object" => followed.ap_id,
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
%Pleroma.Activity{
data: data,
actor: follower.ap_id
}
end
- def websub_subscription_factory do
- %Pleroma.Web.Websub.WebsubServerSubscription{
- topic: "http://example.org",
- callback: "http://example.org/callback",
- secret: "here's a secret",
- valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 100),
- state: "requested"
- }
- end
-
- def websub_client_subscription_factory do
- %Pleroma.Web.Websub.WebsubClientSubscription{
- topic: "http://example.org",
- secret: "here's a secret",
- valid_until: nil,
- state: "requested",
- subscribers: []
- }
- end
-
def oauth_app_factory do
%Pleroma.Web.OAuth.App{
client_name: "Some client",
redirect_uris: "https://example.com/callback",
scopes: ["read", "write", "follow", "push"],
website: "https://example.com",
client_id: Ecto.UUID.generate(),
client_secret: "aaa;/&bbb"
}
end
def instance_factory do
%Pleroma.Instances.Instance{
host: "domain.com",
unreachable_since: nil
}
end
def oauth_token_factory do
oauth_app = insert(:oauth_app)
%Pleroma.Web.OAuth.Token{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
scopes: ["read"],
refresh_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
user: build(:user),
app_id: oauth_app.id,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
}
end
def oauth_authorization_factory do
%Pleroma.Web.OAuth.Authorization{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false),
scopes: ["read", "write", "follow", "push"],
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10),
user: build(:user),
app: build(:oauth_app)
}
end
def push_subscription_factory do
%Pleroma.Web.Push.Subscription{
user: build(:user),
token: build(:oauth_token),
endpoint: "https://example.com/example/1234",
key_auth: "8eDyX_uCN0XRhSbY5hs7Hg==",
key_p256dh:
"BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=",
data: %{}
}
end
def notification_factory do
%Pleroma.Notification{
user: build(:user)
}
end
def scheduled_activity_factory do
%Pleroma.ScheduledActivity{
user: build(:user),
scheduled_at: NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(60), :millisecond),
params: build(:note) |> Map.from_struct() |> Map.get(:data)
}
end
def registration_factory do
user = insert(:user)
%Pleroma.Registration{
user: user,
provider: "twitter",
uid: "171799000",
info: %{
"name" => "John Doe",
"email" => "john@doe.com",
"nickname" => "johndoe",
"description" => "My bio"
}
}
end
def config_factory do
%Pleroma.Web.AdminAPI.Config{
key: sequence(:key, &"some_key_#{&1}"),
group: "pleroma",
value:
sequence(
:value,
fn key ->
:erlang.term_to_binary(%{another_key: "#{key}somevalue", another: "#{key}somevalue"})
end
)
}
end
+
+ def marker_factory do
+ %Pleroma.Marker{
+ user: build(:user),
+ timeline: "notifications",
+ lock_version: 0,
+ last_read_id: "1"
+ }
+ end
end
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 4feb57f3a..eba22c40b 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -1,1272 +1,1288 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule HttpRequestMock do
require Logger
def request(
%Tesla.Env{
url: url,
method: method,
headers: headers,
query: query,
body: body
} = _env
) do
with {:ok, res} <- apply(__MODULE__, method, [url, query, body, headers]) do
res
else
error ->
with {:error, message} <- error do
Logger.warn(message)
end
{_, _r} = error
end
end
# GET Requests
#
def get(url, query \\ [], body \\ [], headers \\ [])
def get("https://osada.macgirvin.com/channel/mike", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___osada.macgirvin.com_channel_mike.json")
}}
end
+ def get("https://shitposter.club/users/moonman", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/moonman@shitposter.club.json")
+ }}
+ end
+
def get("https://mastodon.social/users/emelie/statuses/101849165031453009", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/status.emelie.json")
}}
end
def get("https://mastodon.social/users/emelie/statuses/101849165031453404", _, _, _) do
{:ok,
%Tesla.Env{
status: 404,
body: ""
}}
end
def get("https://mastodon.social/users/emelie", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/emelie.json")
}}
end
def get("https://mastodon.social/users/not_found", _, _, _) do
{:ok, %Tesla.Env{status: 404}}
end
def get("https://mastodon.sdf.org/users/rinpatch", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/rinpatch.json")
}}
end
def get(
"https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/emelie",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/webfinger_emelie.json")
}}
end
def get("https://mastodon.social/users/emelie.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/emelie.atom")
}}
end
def get(
"https://osada.macgirvin.com/.well-known/webfinger?resource=acct:mike@osada.macgirvin.com",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mike@osada.macgirvin.com.json")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/29191",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___social.heldscal.la_user_29191.xml")
}}
end
def get("https://pawoo.net/users/pekorino.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___pawoo.net_users_pekorino.atom")
}}
end
def get(
"https://pawoo.net/.well-known/webfinger?resource=acct:https://pawoo.net/users/pekorino",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___pawoo.net_users_pekorino.xml")
}}
end
def get(
"https://social.stopwatchingus-heidelberg.de/api/statuses/user_timeline/18330.atom",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/atarifrosch_feed.xml")
}}
end
def get(
"https://social.stopwatchingus-heidelberg.de/.well-known/webfinger?resource=acct:https://social.stopwatchingus-heidelberg.de/user/18330",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/atarifrosch_webfinger.xml")
}}
end
def get("https://mamot.fr/users/Skruyb.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___mamot.fr_users_Skruyb.atom")
}}
end
def get(
"https://mamot.fr/.well-known/webfinger?resource=acct:https://mamot.fr/users/Skruyb",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/skruyb@mamot.fr.atom")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=nonexistant@social.heldscal.la",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/nonexistant@social.heldscal.la.xml")
}}
end
def get(
"https://squeet.me/xrd/?uri=lain@squeet.me",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/lain_squeet.me_webfinger.xml")
}}
end
def get(
"https://mst3k.interlinked.me/users/luciferMysticus",
_,
_,
Accept: "application/activity+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/lucifermysticus.json")
}}
end
def get("https://prismo.news/@mxb", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___prismo.news__mxb.json")
}}
end
def get(
"https://hubzilla.example.org/channel/kaniini",
_,
_,
Accept: "application/activity+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/kaniini@hubzilla.example.org.json")
}}
end
def get("https://niu.moe/users/rye", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/rye.json")
}}
end
def get("https://n1u.moe/users/rye", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/rye.json")
}}
end
def get("http://mastodon.example.org/users/admin/statuses/100787282858396771", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/tesla_mock/http___mastodon.example.org_users_admin_status_1234.json"
)
}}
end
def get("https://puckipedia.com/", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/puckipedia.com.json")
}}
end
def get("https://peertube.moe/accounts/7even", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/7even.json")
}}
end
def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/peertube.moe-vid.json")
}}
end
def get("https://baptiste.gelez.xyz/@/BaptisteGelez", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-user.json")
}}
end
def get("https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/baptiste.gelex.xyz-article.json")
}}
end
def get("https://wedistribute.org/wp-json/pterotype/v1/object/85810", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/wedistribute-article.json")
}}
end
def get("https://wedistribute.org/wp-json/pterotype/v1/actor/-blog", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json")
}}
end
def get("http://mastodon.example.org/users/admin", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/admin@mastdon.example.org.json")
}}
end
+ def get("http://mastodon.example.org/users/relay", _, _, Accept: "application/activity+json") do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/relay@mastdon.example.org.json")
+ }}
+ end
+
def get("http://mastodon.example.org/users/gargron", _, _, Accept: "application/activity+json") do
{:error, :nxdomain}
end
def get("http://osada.macgirvin.com/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 404,
body: ""
}}
end
def get("https://osada.macgirvin.com/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 404,
body: ""
}}
end
def get("http://mastodon.sdf.org/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/sdf.org_host_meta")
}}
end
def get("https://mastodon.sdf.org/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/sdf.org_host_meta")
}}
end
def get(
"https://mastodon.sdf.org/.well-known/webfinger?resource=https://mastodon.sdf.org/users/snowdusk",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/snowdusk@sdf.org_host_meta.json")
}}
end
def get("http://mstdn.jp/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mstdn.jp_host_meta")
}}
end
def get("https://mstdn.jp/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mstdn.jp_host_meta")
}}
end
def get("https://mstdn.jp/.well-known/webfinger?resource=kpherox@mstdn.jp", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/kpherox@mstdn.jp.xml")
}}
end
def get("http://mamot.fr/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mamot.fr_host_meta")
}}
end
def get("https://mamot.fr/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mamot.fr_host_meta")
}}
end
def get(
"https://mamot.fr/.well-known/webfinger?resource=https://mamot.fr/users/Skruyb",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/skruyb@mamot.fr.atom")
}}
end
def get("http://pawoo.net/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/pawoo.net_host_meta")
}}
end
def get("https://pawoo.net/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/pawoo.net_host_meta")
}}
end
def get(
"https://pawoo.net/.well-known/webfinger?resource=https://pawoo.net/users/pekorino",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/pekorino@pawoo.net_host_meta.json")
}}
end
def get("http://zetsubou.xn--q9jyb4c/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/xn--q9jyb4c_host_meta")
}}
end
def get("https://zetsubou.xn--q9jyb4c/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/xn--q9jyb4c_host_meta")
}}
end
def get("http://pleroma.soykaf.com/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/soykaf.com_host_meta")
}}
end
def get("https://pleroma.soykaf.com/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/soykaf.com_host_meta")
}}
end
def get("http://social.stopwatchingus-heidelberg.de/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/stopwatchingus-heidelberg.de_host_meta")
}}
end
def get("https://social.stopwatchingus-heidelberg.de/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/stopwatchingus-heidelberg.de_host_meta")
}}
end
def get(
"http://mastodon.example.org/@admin/99541947525187367",
_,
_,
Accept: "application/activity+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/mastodon-note-object.json")
}}
end
def get("http://mastodon.example.org/@admin/99541947525187368", _, _, _) do
{:ok,
%Tesla.Env{
status: 404,
body: ""
}}
end
def get("https://shitposter.club/notice/7369654", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/7369654.html")
}}
end
def get("https://mstdn.io/users/mayuutann", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mayumayu.json")
}}
end
def get(
"https://mstdn.io/users/mayuutann/statuses/99568293732299394",
_,
_,
Accept: "application/activity+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mayumayupost.json")
}}
end
def get("https://pleroma.soykaf.com/users/lain/feed.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/tesla_mock/https___pleroma.soykaf.com_users_lain_feed.atom.xml"
)
}}
end
def get(url, _, _, Accept: "application/xrd+xml,application/jrd+json")
when url in [
"https://pleroma.soykaf.com/.well-known/webfinger?resource=acct:https://pleroma.soykaf.com/users/lain",
"https://pleroma.soykaf.com/.well-known/webfinger?resource=https://pleroma.soykaf.com/users/lain"
] do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___pleroma.soykaf.com_users_lain.xml")
}}
end
def get("https://shitposter.club/api/statuses/user_timeline/1.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/tesla_mock/https___shitposter.club_api_statuses_user_timeline_1.atom.xml"
)
}}
end
def get(
"https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/1",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___shitposter.club_user_1.xml")
}}
end
def get("https://shitposter.club/notice/2827873", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
- body: File.read!("test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.html")
+ body: File.read!("test/fixtures/tesla_mock/https___shitposter.club_notice_2827873.json")
}}
end
def get("https://shitposter.club/api/statuses/show/2827873.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/tesla_mock/https___shitposter.club_api_statuses_show_2827873.atom.xml"
)
}}
end
def get("https://testing.pleroma.lol/objects/b319022a-4946-44c5-9de9-34801f95507b", _, _, _) do
{:ok, %Tesla.Env{status: 200}}
end
def get("https://shitposter.club/api/statuses/user_timeline/5381.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/spc_5381.atom")
}}
end
def get(
"https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/5381",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/spc_5381_xrd.xml")
}}
end
def get("http://shitposter.club/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/shitposter.club_host_meta")
}}
end
def get("https://shitposter.club/api/statuses/show/7369654.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/7369654.atom")
}}
end
def get("https://shitposter.club/notice/4027863", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/7369654.html")
}}
end
def get("https://social.sakamoto.gq/users/eal/feed.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/sakamoto_eal_feed.atom")
}}
end
def get("http://social.sakamoto.gq/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/social.sakamoto.gq_host_meta")
}}
end
def get(
"https://social.sakamoto.gq/.well-known/webfinger?resource=https://social.sakamoto.gq/users/eal",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/eal_sakamoto.xml")
}}
end
def get(
"https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056",
_,
_,
Accept: "application/atom+xml"
) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/sakamoto.atom")}}
end
def get("http://mastodon.social/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/mastodon.social_host_meta")
}}
end
def get(
"https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/lambadalambda",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/tesla_mock/https___mastodon.social_users_lambadalambda.xml")
}}
end
def get("http://gs.example.org/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/gs.example.org_host_meta")
}}
end
def get(
"http://gs.example.org/.well-known/webfinger?resource=http://gs.example.org:4040/index.php/user/1",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!("test/fixtures/tesla_mock/http___gs.example.org_4040_index.php_user_1.xml")
}}
end
def get(
"http://gs.example.org:4040/index.php/user/1",
_,
_,
Accept: "application/activity+json"
) do
{:ok, %Tesla.Env{status: 406, body: ""}}
end
def get("http://gs.example.org/index.php/api/statuses/user_timeline/1.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/tesla_mock/http__gs.example.org_index.php_api_statuses_user_timeline_1.atom.xml"
)
}}
end
def get("https://social.heldscal.la/api/statuses/user_timeline/29191.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_29191.atom.xml"
)
}}
end
def get("http://squeet.me/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/squeet.me_host_meta")}}
end
def get(
"https://squeet.me/xrd?uri=lain@squeet.me",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/lain_squeet.me_webfinger.xml")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=shp@social.heldscal.la",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/shp@social.heldscal.la.xml")
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=invalid_content@social.heldscal.la",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok, %Tesla.Env{status: 200, body: ""}}
end
def get("http://framatube.org/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/framatube.org_host_meta")
}}
end
def get(
"http://framatube.org/main/xrd?uri=framasoft@framatube.org",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/json"}],
body: File.read!("test/fixtures/tesla_mock/framasoft@framatube.org.json")
}}
end
def get("http://gnusocial.de/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/gnusocial.de_host_meta")
}}
end
def get(
"http://gnusocial.de/main/xrd?uri=winterdienst@gnusocial.de",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/winterdienst_webfinger.json")
}}
end
def get("http://status.alpicola.com/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/status.alpicola.com_host_meta")
}}
end
def get("http://macgirvin.com/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/macgirvin.com_host_meta")
}}
end
def get("http://gerzilla.de/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/gerzilla.de_host_meta")
}}
end
def get(
"https://gerzilla.de/xrd/?uri=kaniini@gerzilla.de",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/json"}],
body: File.read!("test/fixtures/tesla_mock/kaniini@gerzilla.de.json")
}}
end
def get("https://social.heldscal.la/api/statuses/user_timeline/23211.atom", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body:
File.read!(
"test/fixtures/tesla_mock/https___social.heldscal.la_api_statuses_user_timeline_23211.atom.xml"
)
}}
end
def get(
"https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/23211",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https___social.heldscal.la_user_23211.xml")
}}
end
def get("http://social.heldscal.la/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/social.heldscal.la_host_meta")
}}
end
def get("https://social.heldscal.la/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/social.heldscal.la_host_meta")
}}
end
def get("https://mastodon.social/users/lambadalambda.atom", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.atom")}}
end
def get("https://mastodon.social/users/lambadalambda", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.json")}}
end
def get("https://apfed.club/channel/indio", _, _, _) do
{:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/osada-user-indio.json")}}
end
def get("https://social.heldscal.la/user/23211", _, _, Accept: "application/activity+json") do
{:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)}
end
def get("http://example.com/ogp", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}}
end
def get("https://example.com/ogp", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}}
end
def get("https://pleroma.local/notice/9kCP7V", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}}
end
def get("http://localhost:4001/users/masto_closed/followers", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/users_mock/masto_closed_followers.json")
}}
end
def get("http://localhost:4001/users/masto_closed/followers?page=1", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/users_mock/masto_closed_followers_page.json")
}}
end
def get("http://localhost:4001/users/masto_closed/following", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/users_mock/masto_closed_following.json")
}}
end
def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/users_mock/masto_closed_following_page.json")
}}
end
def get("http://localhost:4001/users/fuser2/followers", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/users_mock/pleroma_followers.json")
}}
end
def get("http://localhost:4001/users/fuser2/following", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/users_mock/pleroma_following.json")
}}
end
def get("http://domain-with-errors:4001/users/fuser1/followers", _, _, _) do
{:ok,
%Tesla.Env{
status: 504,
body: ""
}}
end
def get("http://domain-with-errors:4001/users/fuser1/following", _, _, _) do
{:ok,
%Tesla.Env{
status: 504,
body: ""
}}
end
def get("http://example.com/ogp-missing-data", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/rich_media/ogp-missing-data.html")
}}
end
def get("https://example.com/ogp-missing-data", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/rich_media/ogp-missing-data.html")
}}
end
def get("http://example.com/malformed", _, _, _) do
{:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/malformed-data.html")}}
end
def get("http://example.com/empty", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: "hello"}}
end
def get("http://404.site" <> _, _, _, _) do
{:ok,
%Tesla.Env{
status: 404,
body: ""
}}
end
def get(
"https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=lain@zetsubou.xn--q9jyb4c",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/lain.xml")
}}
end
def get(
"https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=https://zetsubou.xn--q9jyb4c/users/lain",
_,
_,
Accept: "application/xrd+xml,application/jrd+json"
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/lain.xml")
}}
end
def get(
"https://zetsubou.xn--q9jyb4c/.well-known/host-meta",
_,
_,
_
) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/host-meta-zetsubou.xn--q9jyb4c.xml")
}}
end
def get("https://info.pleroma.site/activity.json", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity.json")
}}
end
def get("https://info.pleroma.site/activity.json", _, _, _) do
{:ok, %Tesla.Env{status: 404, body: ""}}
end
def get("https://info.pleroma.site/activity2.json", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity2.json")
}}
end
def get("https://info.pleroma.site/activity2.json", _, _, _) do
{:ok, %Tesla.Env{status: 404, body: ""}}
end
def get("https://info.pleroma.site/activity3.json", _, _, Accept: "application/activity+json") do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/https__info.pleroma.site_activity3.json")
}}
end
def get("https://info.pleroma.site/activity3.json", _, _, _) do
{:ok, %Tesla.Env{status: 404, body: ""}}
end
def get("https://mstdn.jp/.well-known/webfinger?resource=acct:kpherox@mstdn.jp", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/kpherox@mstdn.jp.xml")
}}
end
def get("http://example.com/rel_me/anchor", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor.html")}}
end
def get("http://example.com/rel_me/anchor_nofollow", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor_nofollow.html")}}
end
def get("http://example.com/rel_me/link", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_link.html")}}
end
def get("http://example.com/rel_me/null", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_null.html")}}
end
def get("https://skippers-bin.com/notes/7x9tmrp97i", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/misskey_poll_no_end_date.json")
}}
end
def get("https://skippers-bin.com/users/7v1w1r8ce6", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/sjw.json")}}
end
def get("https://patch.cx/users/rin", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/rin.json")}}
end
def get(url, query, body, headers) do
{:error,
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{
inspect(headers)
}"}
end
# POST Requests
#
def post(url, query \\ [], body \\ [], headers \\ [])
def post("http://example.org/needs_refresh", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: ""
}}
end
def post("http://mastodon.example.org/inbox", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: ""
}}
end
def post("https://hubzilla.example.org/inbox", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: ""
}}
end
def post("http://gs.example.org/index.php/main/salmon/user/1", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: ""
}}
end
def post("http://200.site" <> _, _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: ""
}}
end
def post("http://connrefused.site" <> _, _, _, _) do
{:error, :connrefused}
end
def post("http://404.site" <> _, _, _, _) do
{:ok,
%Tesla.Env{
status: 404,
body: ""
}}
end
def post(url, query, body, headers) do
{:error,
"Mock response not implemented for POST #{inspect(url)}, #{query}, #{inspect(body)}, #{
inspect(headers)
}"}
end
end
diff --git a/test/tasks/count_statuses_test.exs b/test/tasks/count_statuses_test.exs
index 6035da3c3..bb5dc88f8 100644
--- a/test/tasks/count_statuses_test.exs
+++ b/test/tasks/count_statuses_test.exs
@@ -1,39 +1,39 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.CountStatusesTest do
use Pleroma.DataCase
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import ExUnit.CaptureIO, only: [capture_io: 1]
import Pleroma.Factory
test "counts statuses" do
user = insert(:user)
{:ok, _} = CommonAPI.post(user, %{"status" => "test"})
{:ok, _} = CommonAPI.post(user, %{"status" => "test2"})
user2 = insert(:user)
{:ok, _} = CommonAPI.post(user2, %{"status" => "test3"})
user = refresh_record(user)
user2 = refresh_record(user2)
- assert %{info: %{note_count: 2}} = user
- assert %{info: %{note_count: 1}} = user2
+ assert %{note_count: 2} = user
+ assert %{note_count: 1} = user2
- {:ok, user} = User.update_info(user, &User.Info.set_note_count(&1, 0))
- {:ok, user2} = User.update_info(user2, &User.Info.set_note_count(&1, 0))
+ {:ok, user} = User.update_note_count(user, 0)
+ {:ok, user2} = User.update_note_count(user2, 0)
- assert %{info: %{note_count: 0}} = user
- assert %{info: %{note_count: 0}} = user2
+ assert %{note_count: 0} = user
+ assert %{note_count: 0} = user2
assert capture_io(fn -> Mix.Tasks.Pleroma.CountStatuses.run([]) end) == "Done\n"
- assert %{info: %{note_count: 2}} = refresh_record(user)
- assert %{info: %{note_count: 1}} = refresh_record(user2)
+ assert %{note_count: 2} = refresh_record(user)
+ assert %{note_count: 1} = refresh_record(user2)
end
end
diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs
index b63dcac00..b66324e5e 100644
--- a/test/tasks/database_test.exs
+++ b/test/tasks/database_test.exs
@@ -1,130 +1,129 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.DatabaseTest do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
use Pleroma.DataCase
import Pleroma.Factory
setup_all do
Mix.shell(Mix.Shell.Process)
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
end)
:ok
end
describe "running remove_embedded_objects" do
test "it replaces objects with references" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
new_data = Map.put(activity.data, "object", activity.object.data)
{:ok, activity} =
activity
|> Activity.change(%{data: new_data})
|> Repo.update()
assert is_map(activity.data["object"])
Mix.Tasks.Pleroma.Database.run(["remove_embedded_objects"])
activity = Activity.get_by_id_with_object(activity.id)
assert is_binary(activity.data["object"])
end
end
describe "prune_objects" do
test "it prunes old objects from the database" do
insert(:note)
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1
date =
Timex.now()
|> Timex.shift(days: -deadline)
|> Timex.to_naive_datetime()
|> NaiveDateTime.truncate(:second)
%{id: id} =
:note
|> insert()
|> Ecto.Changeset.change(%{inserted_at: date})
|> Repo.update!()
assert length(Repo.all(Object)) == 2
Mix.Tasks.Pleroma.Database.run(["prune_objects"])
assert length(Repo.all(Object)) == 1
refute Object.get_by_id(id)
end
end
describe "running update_users_following_followers_counts" do
test "following and followers count are updated" do
[user, user2] = insert_pair(:user)
- {:ok, %User{following: following, info: info} = user} = User.follow(user, user2)
+ {:ok, %User{following: following} = user} = User.follow(user, user2)
assert length(following) == 2
- assert info.follower_count == 0
+ assert user.follower_count == 0
{:ok, user} =
user
- |> Ecto.Changeset.change(%{following: following ++ following})
- |> User.change_info(&Ecto.Changeset.change(&1, %{follower_count: 3}))
+ |> Ecto.Changeset.change(%{following: following ++ following, follower_count: 3})
|> Repo.update()
assert length(user.following) == 4
- assert user.info.follower_count == 3
+ assert user.follower_count == 3
assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"])
user = User.get_by_id(user.id)
assert length(user.following) == 2
- assert user.info.follower_count == 0
+ assert user.follower_count == 0
end
end
describe "running fix_likes_collections" do
test "it turns OrderedCollection likes into empty arrays" do
[user, user2] = insert_pair(:user)
{:ok, %{id: id, object: object}} = CommonAPI.post(user, %{"status" => "test"})
{:ok, %{object: object2}} = CommonAPI.post(user, %{"status" => "test test"})
CommonAPI.favorite(id, user2)
likes = %{
"first" =>
"http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1",
"id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes",
"totalItems" => 3,
"type" => "OrderedCollection"
}
new_data = Map.put(object2.data, "likes", likes)
object2
|> Ecto.Changeset.change(%{data: new_data})
|> Repo.update()
assert length(Object.get_by_id(object.id).data["likes"]) == 1
assert is_map(Object.get_by_id(object2.id).data["likes"])
assert :ok == Mix.Tasks.Pleroma.Database.run(["fix_likes_collections"])
assert length(Object.get_by_id(object.id).data["likes"]) == 1
assert Enum.empty?(Object.get_by_id(object2.id).data["likes"])
end
end
end
diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs
index cf12d9ed6..f024f92ae 100644
--- a/test/tasks/user_test.exs
+++ b/test/tasks/user_test.exs
@@ -1,473 +1,473 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.UserTest do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
use Pleroma.DataCase
import Pleroma.Factory
import ExUnit.CaptureIO
setup_all do
Mix.shell(Mix.Shell.Process)
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
end)
:ok
end
describe "running new" do
test "user is created" do
# just get random data
unsaved = build(:user)
# prepare to answer yes
send(self(), {:mix_shell_input, :yes?, true})
Mix.Tasks.Pleroma.User.run([
"new",
unsaved.nickname,
unsaved.email,
"--name",
unsaved.name,
"--bio",
unsaved.bio,
"--password",
"test",
"--moderator",
"--admin"
])
assert_received {:mix_shell, :info, [message]}
assert message =~ "user will be created"
assert_received {:mix_shell, :yes?, [message]}
assert message =~ "Continue"
assert_received {:mix_shell, :info, [message]}
assert message =~ "created"
user = User.get_cached_by_nickname(unsaved.nickname)
assert user.name == unsaved.name
assert user.email == unsaved.email
assert user.bio == unsaved.bio
- assert user.info.is_moderator
- assert user.info.is_admin
+ assert user.is_moderator
+ assert user.is_admin
end
test "user is not created" do
unsaved = build(:user)
# prepare to answer no
send(self(), {:mix_shell_input, :yes?, false})
Mix.Tasks.Pleroma.User.run(["new", unsaved.nickname, unsaved.email])
assert_received {:mix_shell, :info, [message]}
assert message =~ "user will be created"
assert_received {:mix_shell, :yes?, [message]}
assert message =~ "Continue"
assert_received {:mix_shell, :info, [message]}
assert message =~ "will not be created"
refute User.get_cached_by_nickname(unsaved.nickname)
end
end
describe "running rm" do
test "user is deleted" do
user = insert(:user)
Mix.Tasks.Pleroma.User.run(["rm", user.nickname])
assert_received {:mix_shell, :info, [message]}
assert message =~ " deleted"
refute User.get_by_nickname(user.nickname)
end
test "no user to delete" do
Mix.Tasks.Pleroma.User.run(["rm", "nonexistent"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No local user"
end
end
describe "running toggle_activated" do
test "user is deactivated" do
user = insert(:user)
Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname])
assert_received {:mix_shell, :info, [message]}
assert message =~ " deactivated"
user = User.get_cached_by_nickname(user.nickname)
- assert user.info.deactivated
+ assert user.deactivated
end
test "user is activated" do
- user = insert(:user, info: %{deactivated: true})
+ user = insert(:user, deactivated: true)
Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname])
assert_received {:mix_shell, :info, [message]}
assert message =~ " activated"
user = User.get_cached_by_nickname(user.nickname)
- refute user.info.deactivated
+ refute user.deactivated
end
test "no user to toggle" do
Mix.Tasks.Pleroma.User.run(["toggle_activated", "nonexistent"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No user"
end
end
describe "running unsubscribe" do
test "user is unsubscribed" do
followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]})
Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname])
assert_received {:mix_shell, :info, [message]}
assert message =~ "Deactivating"
assert_received {:mix_shell, :info, [message]}
assert message =~ "Unsubscribing"
# Note that the task has delay :timer.sleep(500)
assert_received {:mix_shell, :info, [message]}
assert message =~ "Successfully unsubscribed"
user = User.get_cached_by_nickname(user.nickname)
assert Enum.empty?(user.following)
- assert user.info.deactivated
+ assert user.deactivated
end
test "no user to unsubscribe" do
Mix.Tasks.Pleroma.User.run(["unsubscribe", "nonexistent"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No user"
end
end
describe "running set" do
test "All statuses set" do
user = insert(:user)
Mix.Tasks.Pleroma.User.run(["set", user.nickname, "--moderator", "--admin", "--locked"])
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Moderator status .* true/
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Locked status .* true/
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Admin status .* true/
user = User.get_cached_by_nickname(user.nickname)
- assert user.info.is_moderator
- assert user.info.locked
- assert user.info.is_admin
+ assert user.is_moderator
+ assert user.locked
+ assert user.is_admin
end
test "All statuses unset" do
- user = insert(:user, info: %{is_moderator: true, locked: true, is_admin: true})
+ user = insert(:user, locked: true, is_moderator: true, is_admin: true)
Mix.Tasks.Pleroma.User.run([
"set",
user.nickname,
"--no-moderator",
"--no-admin",
"--no-locked"
])
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Moderator status .* false/
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Locked status .* false/
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Admin status .* false/
user = User.get_cached_by_nickname(user.nickname)
- refute user.info.is_moderator
- refute user.info.locked
- refute user.info.is_admin
+ refute user.is_moderator
+ refute user.locked
+ refute user.is_admin
end
test "no user to set status" do
Mix.Tasks.Pleroma.User.run(["set", "nonexistent", "--moderator"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No local user"
end
end
describe "running reset_password" do
test "password reset token is generated" do
user = insert(:user)
assert capture_io(fn ->
Mix.Tasks.Pleroma.User.run(["reset_password", user.nickname])
end) =~ "URL:"
assert_received {:mix_shell, :info, [message]}
assert message =~ "Generated"
end
test "no user to reset password" do
Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No local user"
end
end
describe "running invite" do
test "invite token is generated" do
assert capture_io(fn ->
Mix.Tasks.Pleroma.User.run(["invite"])
end) =~ "http"
assert_received {:mix_shell, :info, [message]}
assert message =~ "Generated user invite token one time"
end
test "token is generated with expires_at" do
assert capture_io(fn ->
Mix.Tasks.Pleroma.User.run([
"invite",
"--expires-at",
Date.to_string(Date.utc_today())
])
end)
assert_received {:mix_shell, :info, [message]}
assert message =~ "Generated user invite token date limited"
end
test "token is generated with max use" do
assert capture_io(fn ->
Mix.Tasks.Pleroma.User.run([
"invite",
"--max-use",
"5"
])
end)
assert_received {:mix_shell, :info, [message]}
assert message =~ "Generated user invite token reusable"
end
test "token is generated with max use and expires date" do
assert capture_io(fn ->
Mix.Tasks.Pleroma.User.run([
"invite",
"--max-use",
"5",
"--expires-at",
Date.to_string(Date.utc_today())
])
end)
assert_received {:mix_shell, :info, [message]}
assert message =~ "Generated user invite token reusable date limited"
end
end
describe "running invites" do
test "invites are listed" do
{:ok, invite} = Pleroma.UserInviteToken.create_invite()
{:ok, invite2} =
Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 15})
# assert capture_io(fn ->
Mix.Tasks.Pleroma.User.run([
"invites"
])
# end)
assert_received {:mix_shell, :info, [message]}
assert_received {:mix_shell, :info, [message2]}
assert_received {:mix_shell, :info, [message3]}
assert message =~ "Invites list:"
assert message2 =~ invite.invite_type
assert message3 =~ invite2.invite_type
end
end
describe "running revoke_invite" do
test "invite is revoked" do
{:ok, invite} = Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today()})
assert capture_io(fn ->
Mix.Tasks.Pleroma.User.run([
"revoke_invite",
invite.token
])
end)
assert_received {:mix_shell, :info, [message]}
assert message =~ "Invite for token #{invite.token} was revoked."
end
test "it prints an error message when invite is not exist" do
Mix.Tasks.Pleroma.User.run(["revoke_invite", "foo"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No invite found"
end
end
describe "running delete_activities" do
test "activities are deleted" do
%{nickname: nickname} = insert(:user)
assert :ok == Mix.Tasks.Pleroma.User.run(["delete_activities", nickname])
assert_received {:mix_shell, :info, [message]}
assert message == "User #{nickname} statuses deleted."
end
test "it prints an error message when user is not exist" do
Mix.Tasks.Pleroma.User.run(["delete_activities", "foo"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No local user"
end
end
describe "running toggle_confirmed" do
test "user is confirmed" do
- %{id: id, nickname: nickname} = insert(:user, info: %{confirmation_pending: false})
+ %{id: id, nickname: nickname} = insert(:user, confirmation_pending: false)
assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname])
assert_received {:mix_shell, :info, [message]}
assert message == "#{nickname} needs confirmation."
user = Repo.get(User, id)
- assert user.info.confirmation_pending
- assert user.info.confirmation_token
+ assert user.confirmation_pending
+ assert user.confirmation_token
end
test "user is not confirmed" do
%{id: id, nickname: nickname} =
- insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"})
+ insert(:user, confirmation_pending: true, confirmation_token: "some token")
assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname])
assert_received {:mix_shell, :info, [message]}
assert message == "#{nickname} doesn't need confirmation."
user = Repo.get(User, id)
- refute user.info.confirmation_pending
- refute user.info.confirmation_token
+ refute user.confirmation_pending
+ refute user.confirmation_token
end
test "it prints an error message when user is not exist" do
Mix.Tasks.Pleroma.User.run(["toggle_confirmed", "foo"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No local user"
end
end
describe "search" do
test "it returns users matching" do
user = insert(:user)
moon = insert(:user, nickname: "moon", name: "fediverse expert moon")
moot = insert(:user, nickname: "moot")
kawen = insert(:user, nickname: "kawen", name: "fediverse expert moon")
{:ok, user} = User.follow(user, kawen)
assert [moon.id, kawen.id] == User.Search.search("moon") |> Enum.map(& &1.id)
res = User.search("moo") |> Enum.map(& &1.id)
assert moon.id in res
assert moot.id in res
assert kawen.id in res
assert [moon.id, kawen.id] == User.Search.search("moon fediverse") |> Enum.map(& &1.id)
assert [kawen.id, moon.id] ==
User.Search.search("moon fediverse", for_user: user) |> Enum.map(& &1.id)
end
end
describe "signing out" do
test "it deletes all user's tokens and authorizations" do
user = insert(:user)
insert(:oauth_token, user: user)
insert(:oauth_authorization, user: user)
assert Repo.get_by(Token, user_id: user.id)
assert Repo.get_by(Authorization, user_id: user.id)
:ok = Mix.Tasks.Pleroma.User.run(["sign_out", user.nickname])
refute Repo.get_by(Token, user_id: user.id)
refute Repo.get_by(Authorization, user_id: user.id)
end
test "it prints an error message when user is not exist" do
Mix.Tasks.Pleroma.User.run(["sign_out", "foo"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No local user"
end
end
describe "tagging" do
test "it add tags to a user" do
user = insert(:user)
:ok = Mix.Tasks.Pleroma.User.run(["tag", user.nickname, "pleroma"])
user = User.get_cached_by_nickname(user.nickname)
assert "pleroma" in user.tags
end
test "it prints an error message when user is not exist" do
Mix.Tasks.Pleroma.User.run(["tag", "foo"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "Could not change user tags"
end
end
describe "untagging" do
test "it deletes tags from a user" do
user = insert(:user, tags: ["pleroma"])
assert "pleroma" in user.tags
:ok = Mix.Tasks.Pleroma.User.run(["untag", user.nickname, "pleroma"])
user = User.get_cached_by_nickname(user.nickname)
assert Enum.empty?(user.tags)
end
test "it prints an error message when user is not exist" do
Mix.Tasks.Pleroma.User.run(["untag", "foo"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "Could not change user tags"
end
end
end
diff --git a/test/user_info_test.exs b/test/user_info_test.exs
deleted file mode 100644
index 2d795594e..000000000
--- a/test/user_info_test.exs
+++ /dev/null
@@ -1,24 +0,0 @@
-defmodule Pleroma.UserInfoTest do
- alias Pleroma.Repo
- alias Pleroma.User.Info
-
- use Pleroma.DataCase
-
- import Pleroma.Factory
-
- describe "update_email_notifications/2" do
- setup do
- user = insert(:user, %{info: %{email_notifications: %{"digest" => true}}})
-
- {:ok, user: user}
- end
-
- test "Notifications are updated", %{user: user} do
- true = user.info.email_notifications["digest"]
- changeset = Info.update_email_notifications(user.info, %{"digest" => false})
- assert changeset.valid?
- {:ok, result} = Ecto.Changeset.apply_action(changeset, :insert)
- assert result.email_notifications["digest"] == false
- end
- end
-end
diff --git a/test/user_search_test.exs b/test/user_search_test.exs
index f7ab31287..78a02d536 100644
--- a/test/user_search_test.exs
+++ b/test/user_search_test.exs
@@ -1,311 +1,285 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserSearchTest do
alias Pleroma.Repo
alias Pleroma.User
use Pleroma.DataCase
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "User.search" do
test "accepts limit parameter" do
Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
assert length(User.search("john", limit: 3)) == 3
assert length(User.search("john")) == 5
end
test "accepts offset parameter" do
Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
assert length(User.search("john", limit: 3)) == 3
assert length(User.search("john", limit: 3, offset: 3)) == 2
end
test "finds a user by full or partial nickname" do
user = insert(:user, %{nickname: "john"})
Enum.each(["john", "jo", "j"], fn query ->
assert user ==
User.search(query)
|> List.first()
|> Map.put(:search_rank, nil)
|> Map.put(:search_type, nil)
end)
end
test "finds a user by full or partial name" do
user = insert(:user, %{name: "John Doe"})
Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query ->
assert user ==
User.search(query)
|> List.first()
|> Map.put(:search_rank, nil)
|> Map.put(:search_type, nil)
end)
end
test "finds users, preferring nickname matches over name matches" do
u1 = insert(:user, %{name: "lain", nickname: "nick1"})
u2 = insert(:user, %{nickname: "lain", name: "nick1"})
assert [u2.id, u1.id] == Enum.map(User.search("lain"), & &1.id)
end
test "finds users, considering density of matched tokens" do
u1 = insert(:user, %{name: "Bar Bar plus Word Word"})
u2 = insert(:user, %{name: "Word Word Bar Bar Bar"})
assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
end
- test "finds users, ranking by similarity" do
- u1 = insert(:user, %{name: "lain"})
- _u2 = insert(:user, %{name: "ean"})
- u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
- u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
-
- assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple", for_user: u1), & &1.id)
- end
-
- test "finds users, handling misspelled requests" do
- u1 = insert(:user, %{name: "lain"})
-
- assert [u1.id] == Enum.map(User.search("laiin"), & &1.id)
- end
-
test "finds users, boosting ranks of friends and followers" do
u1 = insert(:user)
u2 = insert(:user, %{name: "Doe"})
follower = insert(:user, %{name: "Doe"})
friend = insert(:user, %{name: "Doe"})
{:ok, follower} = User.follow(follower, u1)
{:ok, u1} = User.follow(u1, friend)
assert [friend.id, follower.id, u2.id] --
Enum.map(User.search("doe", resolve: false, for_user: u1), & &1.id) == []
end
test "finds followers of user by partial name" do
u1 = insert(:user)
u2 = insert(:user, %{name: "Jimi"})
follower_jimi = insert(:user, %{name: "Jimi Hendrix"})
follower_lizz = insert(:user, %{name: "Lizz Wright"})
friend = insert(:user, %{name: "Jimi"})
{:ok, follower_jimi} = User.follow(follower_jimi, u1)
{:ok, _follower_lizz} = User.follow(follower_lizz, u2)
{:ok, u1} = User.follow(u1, friend)
assert Enum.map(User.search("jimi", following: true, for_user: u1), & &1.id) == [
follower_jimi.id
]
assert User.search("lizz", following: true, for_user: u1) == []
end
test "find local and remote users for authenticated users" do
u1 = insert(:user, %{name: "lain"})
u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
results =
"lain"
|> User.search(for_user: u1)
|> Enum.map(& &1.id)
|> Enum.sort()
assert [u1.id, u2.id, u3.id] == results
end
test "find only local users for unauthenticated users" do
%{id: id} = insert(:user, %{name: "lain"})
insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
assert [%{id: ^id}] = User.search("lain")
end
test "find only local users for authenticated users when `limit_to_local_content` is `:all`" do
Pleroma.Config.put([:instance, :limit_to_local_content], :all)
%{id: id} = insert(:user, %{name: "lain"})
insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
assert [%{id: ^id}] = User.search("lain")
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
end
test "find all users for unauthenticated users when `limit_to_local_content` is `false`" do
Pleroma.Config.put([:instance, :limit_to_local_content], false)
u1 = insert(:user, %{name: "lain"})
u2 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social", local: false})
u3 = insert(:user, %{nickname: "lain@pleroma.soykaf.com", local: false})
results =
"lain"
|> User.search()
|> Enum.map(& &1.id)
|> Enum.sort()
assert [u1.id, u2.id, u3.id] == results
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
end
- test "finds a user whose name is nil" do
- _user = insert(:user, %{name: "notamatch", nickname: "testuser@pleroma.amplifie.red"})
- user_two = insert(:user, %{name: nil, nickname: "lain@pleroma.soykaf.com"})
-
- assert user_two ==
- User.search("lain@pleroma.soykaf.com")
- |> List.first()
- |> Map.put(:search_rank, nil)
- |> Map.put(:search_type, nil)
- end
-
test "does not yield false-positive matches" do
insert(:user, %{name: "John Doe"})
Enum.each(["mary", "a", ""], fn query ->
assert [] == User.search(query)
end)
end
test "works with URIs" do
user = insert(:user)
results =
User.search("http://mastodon.example.org/users/admin", resolve: true, for_user: user)
result = results |> List.first()
user = User.get_cached_by_ap_id("http://mastodon.example.org/users/admin")
assert length(results) == 1
expected =
result
|> Map.put(:search_rank, nil)
|> Map.put(:search_type, nil)
|> Map.put(:last_digest_emailed_at, nil)
assert user == expected
end
test "excludes a blocked users from search result" do
user = insert(:user, %{nickname: "Bill"})
[blocked_user | users] = Enum.map(0..3, &insert(:user, %{nickname: "john#{&1}"}))
blocked_user2 =
insert(
:user,
%{nickname: "john awful", ap_id: "https://awful-and-rude-instance.com/user/bully"}
)
User.block_domain(user, "awful-and-rude-instance.com")
User.block(user, blocked_user)
account_ids = User.search("john", for_user: refresh_record(user)) |> collect_ids
assert account_ids == collect_ids(users)
refute Enum.member?(account_ids, blocked_user.id)
refute Enum.member?(account_ids, blocked_user2.id)
assert length(account_ids) == 3
end
test "local user has the same search_rank as for users with the same nickname, but another domain" do
user = insert(:user)
insert(:user, nickname: "lain@mastodon.social")
insert(:user, nickname: "lain")
insert(:user, nickname: "lain@pleroma.social")
assert User.search("lain@localhost", resolve: true, for_user: user)
|> Enum.each(fn u -> u.search_rank == 0.5 end)
end
test "localhost is the part of the domain" do
user = insert(:user)
insert(:user, nickname: "another@somedomain")
insert(:user, nickname: "lain")
insert(:user, nickname: "lain@examplelocalhost")
result = User.search("lain@examplelocalhost", resolve: true, for_user: user)
assert Enum.each(result, fn u -> u.search_rank == 0.5 end)
assert length(result) == 2
end
test "local user search with users" do
user = insert(:user)
local_user = insert(:user, nickname: "lain")
insert(:user, nickname: "another@localhost.com")
insert(:user, nickname: "localhost@localhost.com")
[result] = User.search("lain@localhost", resolve: true, for_user: user)
assert Map.put(result, :search_rank, nil) |> Map.put(:search_type, nil) == local_user
end
test "works with idna domains" do
user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな")))
results = User.search("lain@zetsubou.みんな", resolve: false, for_user: user)
result = List.first(results)
assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
end
test "works with idna domains converted input" do
user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな")))
results =
User.search("lain@zetsubou." <> to_string(:idna.encode("zetsubou.みんな")),
resolve: false,
for_user: user
)
result = List.first(results)
assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
end
test "works with idna domains and bad chars in domain" do
user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな")))
results =
User.search("lain@zetsubou!@#$%^&*()+,-/:;<=>?[]'_{}|~`.みんな",
resolve: false,
for_user: user
)
result = List.first(results)
assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
end
test "works with idna domains and query as link" do
user = insert(:user, nickname: "lain@" <> to_string(:idna.encode("zetsubou.みんな")))
results =
User.search("https://zetsubou.みんな/users/lain",
resolve: false,
for_user: user
)
result = List.first(results)
assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
end
end
end
diff --git a/test/user_test.exs b/test/user_test.exs
index 019e7b400..92de31c74 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1,1785 +1,1755 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserTest do
alias Pleroma.Activity
alias Pleroma.Builders.UserBuilder
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo
import Mock
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
clear_config([:instance, :account_activation_required])
describe "when tags are nil" do
test "tagging a user" do
user = insert(:user, %{tags: nil})
user = User.tag(user, ["cool", "dude"])
assert "cool" in user.tags
assert "dude" in user.tags
end
test "untagging a user" do
user = insert(:user, %{tags: nil})
user = User.untag(user, ["cool", "dude"])
assert user.tags == []
end
end
test "ap_id returns the activity pub id for the user" do
user = UserBuilder.build()
expected_ap_id = "#{Pleroma.Web.base_url()}/users/#{user.nickname}"
assert expected_ap_id == User.ap_id(user)
end
test "ap_followers returns the followers collection for the user" do
user = UserBuilder.build()
expected_followers_collection = "#{User.ap_id(user)}/followers"
assert expected_followers_collection == User.ap_followers(user)
end
test "ap_following returns the following collection for the user" do
user = UserBuilder.build()
expected_followers_collection = "#{User.ap_id(user)}/following"
assert expected_followers_collection == User.ap_following(user)
end
test "returns all pending follow requests" do
unlocked = insert(:user)
- locked = insert(:user, %{info: %{locked: true}})
+ locked = insert(:user, locked: true)
follower = insert(:user)
CommonAPI.follow(follower, unlocked)
CommonAPI.follow(follower, locked)
assert [] = User.get_follow_requests(unlocked)
assert [activity] = User.get_follow_requests(locked)
assert activity
end
test "doesn't return already accepted or duplicate follow requests" do
- locked = insert(:user, %{info: %{locked: true}})
+ locked = insert(:user, locked: true)
pending_follower = insert(:user)
accepted_follower = insert(:user)
CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(accepted_follower, locked)
User.follow(accepted_follower, locked)
assert [activity] = User.get_follow_requests(locked)
assert activity
end
test "clears follow requests when requester is blocked" do
- followed = insert(:user, %{info: %{locked: true}})
+ followed = insert(:user, locked: true)
follower = insert(:user)
CommonAPI.follow(follower, followed)
assert [_activity] = User.get_follow_requests(followed)
{:ok, _follower} = User.block(followed, follower)
assert [] = User.get_follow_requests(followed)
end
test "follow_all follows mutliple users" do
user = insert(:user)
followed_zero = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
blocked = insert(:user)
not_followed = insert(:user)
reverse_blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
{:ok, reverse_blocked} = User.block(reverse_blocked, user)
{:ok, user} = User.follow(user, followed_zero)
{:ok, user} = User.follow_all(user, [followed_one, followed_two, blocked, reverse_blocked])
assert User.following?(user, followed_one)
assert User.following?(user, followed_two)
assert User.following?(user, followed_zero)
refute User.following?(user, not_followed)
refute User.following?(user, blocked)
refute User.following?(user, reverse_blocked)
end
test "follow_all follows mutliple users without duplicating" do
user = insert(:user)
followed_zero = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
{:ok, user} = User.follow_all(user, [followed_zero, followed_one])
assert length(user.following) == 3
{:ok, user} = User.follow_all(user, [followed_one, followed_two])
assert length(user.following) == 4
end
test "follow takes a user and another user" do
user = insert(:user)
followed = insert(:user)
{:ok, user} = User.follow(user, followed)
user = User.get_cached_by_id(user.id)
followed = User.get_cached_by_ap_id(followed.ap_id)
- assert followed.info.follower_count == 1
+ assert followed.follower_count == 1
assert User.ap_followers(followed) in user.following
end
test "can't follow a deactivated users" do
user = insert(:user)
- followed = insert(:user, info: %{deactivated: true})
+ followed = insert(:user, %{deactivated: true})
{:error, _} = User.follow(user, followed)
end
test "can't follow a user who blocked us" do
blocker = insert(:user)
blockee = insert(:user)
{:ok, blocker} = User.block(blocker, blockee)
{:error, _} = User.follow(blockee, blocker)
end
test "can't subscribe to a user who blocked us" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocker} = User.block(blocker, blocked)
{:error, _} = User.subscribe(blocked, blocker)
end
test "local users do not automatically follow local locked accounts" do
- follower = insert(:user, info: %{locked: true})
- followed = insert(:user, info: %{locked: true})
+ follower = insert(:user, locked: true)
+ followed = insert(:user, locked: true)
{:ok, follower} = User.maybe_direct_follow(follower, followed)
refute User.following?(follower, followed)
end
- # This is a somewhat useless test.
- # test "following a remote user will ensure a websub subscription is present" do
- # user = insert(:user)
- # {:ok, followed} = OStatus.make_user("shp@social.heldscal.la")
-
- # assert followed.local == false
-
- # {:ok, user} = User.follow(user, followed)
- # assert User.ap_followers(followed) in user.following
-
- # query = from w in WebsubClientSubscription,
- # where: w.topic == ^followed.info["topic"]
- # websub = Repo.one(query)
-
- # assert websub
- # end
-
describe "unfollow/2" do
setup do
setting = Pleroma.Config.get([:instance, :external_user_synchronization])
on_exit(fn ->
Pleroma.Config.put([:instance, :external_user_synchronization], setting)
end)
:ok
end
test "unfollow with syncronizes external user" do
Pleroma.Config.put([:instance, :external_user_synchronization], true)
followed =
insert(:user,
nickname: "fuser1",
follower_address: "http://localhost:4001/users/fuser1/followers",
following_address: "http://localhost:4001/users/fuser1/following",
ap_id: "http://localhost:4001/users/fuser1"
)
user =
insert(:user, %{
local: false,
nickname: "fuser2",
ap_id: "http://localhost:4001/users/fuser2",
follower_address: "http://localhost:4001/users/fuser2/followers",
following_address: "http://localhost:4001/users/fuser2/following",
following: [User.ap_followers(followed)]
})
{:ok, user, _activity} = User.unfollow(user, followed)
user = User.get_cached_by_id(user.id)
assert user.following == []
end
test "unfollow takes a user and another user" do
followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]})
{:ok, user, _activity} = User.unfollow(user, followed)
user = User.get_cached_by_id(user.id)
assert user.following == []
end
test "unfollow doesn't unfollow yourself" do
user = insert(:user)
{:error, _} = User.unfollow(user, user)
user = User.get_cached_by_id(user.id)
assert user.following == [user.ap_id]
end
end
test "test if a user is following another user" do
followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]})
assert User.following?(user, followed)
refute User.following?(followed, user)
end
test "fetches correct profile for nickname beginning with number" do
# Use old-style integer ID to try to reproduce the problem
user = insert(:user, %{id: 1080})
user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"})
assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname)
end
describe "user registration" do
@full_user_data %{
bio: "A guy",
name: "my name",
nickname: "nick",
password: "test",
password_confirmation: "test",
email: "email@example.com"
}
clear_config([:instance, :autofollowed_nicknames])
clear_config([:instance, :welcome_message])
clear_config([:instance, :welcome_user_nickname])
test "it autofollows accounts that are set for it" do
user = insert(:user)
remote_user = insert(:user, %{local: false})
Pleroma.Config.put([:instance, :autofollowed_nicknames], [
user.nickname,
remote_user.nickname
])
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
assert User.following?(registered_user, user)
refute User.following?(registered_user, remote_user)
end
test "it sends a welcome message if it is set" do
welcome_user = insert(:user)
Pleroma.Config.put([:instance, :welcome_user_nickname], welcome_user.nickname)
Pleroma.Config.put([:instance, :welcome_message], "Hello, this is a cool site")
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
activity = Repo.one(Pleroma.Activity)
assert registered_user.ap_id in activity.recipients
assert Object.normalize(activity).data["content"] =~ "cool site"
assert activity.actor == welcome_user.ap_id
end
test "it requires an email, name, nickname and password, bio is optional" do
@full_user_data
|> Map.keys()
|> Enum.each(fn key ->
params = Map.delete(@full_user_data, key)
changeset = User.register_changeset(%User{}, params)
assert if key == :bio, do: changeset.valid?, else: not changeset.valid?
end)
end
test "it restricts certain nicknames" do
[restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames])
assert is_bitstring(restricted_name)
params =
@full_user_data
|> Map.put(:nickname, restricted_name)
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
end
test "it sets the password_hash, ap_id and following fields" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
assert is_binary(changeset.changes[:password_hash])
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
assert changeset.changes[:following] == [
User.ap_followers(%User{nickname: @full_user_data.nickname})
]
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end
test "it ensures info is not nil" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} =
changeset
|> Repo.insert()
refute is_nil(user.info)
end
end
describe "user registration, with :account_activation_required" do
@full_user_data %{
bio: "A guy",
name: "my name",
nickname: "nick",
password: "test",
password_confirmation: "test",
email: "email@example.com"
}
clear_config([:instance, :account_activation_required]) do
Pleroma.Config.put([:instance, :account_activation_required], true)
end
test "it creates unconfirmed user" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
- assert user.info.confirmation_pending
- assert user.info.confirmation_token
+ assert user.confirmation_pending
+ assert user.confirmation_token
end
test "it creates confirmed user if :confirmed option is given" do
changeset = User.register_changeset(%User{}, @full_user_data, need_confirmation: false)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
- refute user.info.confirmation_pending
- refute user.info.confirmation_token
+ refute user.confirmation_pending
+ refute user.confirmation_token
end
end
describe "get_or_fetch/1" do
test "gets an existing user by nickname" do
user = insert(:user)
{:ok, fetched_user} = User.get_or_fetch(user.nickname)
assert user == fetched_user
end
test "gets an existing user by ap_id" do
ap_id = "http://mastodon.example.org/users/admin"
user =
insert(
:user,
local: false,
nickname: "admin@mastodon.example.org",
ap_id: ap_id,
info: %{}
)
{:ok, fetched_user} = User.get_or_fetch(ap_id)
freshed_user = refresh_record(user)
assert freshed_user == fetched_user
end
end
describe "fetching a user from nickname or trying to build one" do
test "gets an existing user" do
user = insert(:user)
{:ok, fetched_user} = User.get_or_fetch_by_nickname(user.nickname)
assert user == fetched_user
end
test "gets an existing user, case insensitive" do
user = insert(:user, nickname: "nick")
{:ok, fetched_user} = User.get_or_fetch_by_nickname("NICK")
assert user == fetched_user
end
test "gets an existing user by fully qualified nickname" do
user = insert(:user)
{:ok, fetched_user} =
User.get_or_fetch_by_nickname(user.nickname <> "@" <> Pleroma.Web.Endpoint.host())
assert user == fetched_user
end
test "gets an existing user by fully qualified nickname, case insensitive" do
user = insert(:user, nickname: "nick")
casing_altered_fqn = String.upcase(user.nickname <> "@" <> Pleroma.Web.Endpoint.host())
{:ok, fetched_user} = User.get_or_fetch_by_nickname(casing_altered_fqn)
assert user == fetched_user
end
- test "fetches an external user via ostatus if no user exists" do
- {:ok, fetched_user} = User.get_or_fetch_by_nickname("shp@social.heldscal.la")
- assert fetched_user.nickname == "shp@social.heldscal.la"
- end
-
test "returns nil if no user could be fetched" do
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
assert fetched_user == "not found nonexistant@social.heldscal.la"
end
test "returns nil for nonexistant local user" do
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant")
assert fetched_user == "not found nonexistant"
end
test "updates an existing user, if stale" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
orig_user =
insert(
:user,
local: false,
nickname: "admin@mastodon.example.org",
ap_id: "http://mastodon.example.org/users/admin",
last_refreshed_at: a_week_ago,
info: %{}
)
assert orig_user.last_refreshed_at == a_week_ago
{:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
- assert user.info.source_data["endpoints"]
+
+ assert user.source_data["endpoints"]
refute user.last_refreshed_at == orig_user.last_refreshed_at
end
end
test "returns an ap_id for a user" do
user = insert(:user)
assert User.ap_id(user) ==
Pleroma.Web.Router.Helpers.feed_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
)
end
test "returns an ap_followers link for a user" do
user = insert(:user)
assert User.ap_followers(user) ==
Pleroma.Web.Router.Helpers.feed_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
) <> "/followers"
end
describe "remote user creation changeset" do
@valid_remote %{
bio: "hello",
name: "Someone",
nickname: "a@b.de",
ap_id: "http...",
info: %{some: "info"},
avatar: %{some: "avatar"}
}
clear_config([:instance, :user_bio_length])
clear_config([:instance, :user_name_length])
test "it confirms validity" do
cs = User.remote_user_creation(@valid_remote)
assert cs.valid?
end
test "it sets the follower_adress" do
cs = User.remote_user_creation(@valid_remote)
# remote users get a fake local follower address
assert cs.changes.follower_address ==
User.ap_followers(%User{nickname: @valid_remote[:nickname]})
end
test "it enforces the fqn format for nicknames" do
cs = User.remote_user_creation(%{@valid_remote | nickname: "bla"})
assert Ecto.Changeset.get_field(cs, :local) == false
assert cs.changes.avatar
refute cs.valid?
end
test "it has required fields" do
[:name, :ap_id]
|> Enum.each(fn field ->
cs = User.remote_user_creation(Map.delete(@valid_remote, field))
refute cs.valid?
end)
end
end
describe "followers and friends" do
test "gets all followers for a given user" do
user = insert(:user)
follower_one = insert(:user)
follower_two = insert(:user)
not_follower = insert(:user)
{:ok, follower_one} = User.follow(follower_one, user)
{:ok, follower_two} = User.follow(follower_two, user)
res = User.get_followers(user)
assert Enum.member?(res, follower_one)
assert Enum.member?(res, follower_two)
refute Enum.member?(res, not_follower)
end
test "gets all friends (followed users) for a given user" do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
res = User.get_friends(user)
followed_one = User.get_cached_by_ap_id(followed_one.ap_id)
followed_two = User.get_cached_by_ap_id(followed_two.ap_id)
assert Enum.member?(res, followed_one)
assert Enum.member?(res, followed_two)
refute Enum.member?(res, not_followed)
end
end
describe "updating note and follower count" do
- test "it sets the info->note_count property" do
+ test "it sets the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
- assert user.info.note_count == 0
+ assert user.note_count == 0
{:ok, user} = User.update_note_count(user)
- assert user.info.note_count == 1
+ assert user.note_count == 1
end
- test "it increases the info->note_count property" do
+ test "it increases the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
- assert user.info.note_count == 0
+ assert user.note_count == 0
{:ok, user} = User.increase_note_count(user)
- assert user.info.note_count == 1
+ assert user.note_count == 1
{:ok, user} = User.increase_note_count(user)
- assert user.info.note_count == 2
+ assert user.note_count == 2
end
- test "it decreases the info->note_count property" do
+ test "it decreases the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
- assert user.info.note_count == 0
+ assert user.note_count == 0
{:ok, user} = User.increase_note_count(user)
- assert user.info.note_count == 1
+ assert user.note_count == 1
{:ok, user} = User.decrease_note_count(user)
- assert user.info.note_count == 0
+ assert user.note_count == 0
{:ok, user} = User.decrease_note_count(user)
- assert user.info.note_count == 0
+ assert user.note_count == 0
end
- test "it sets the info->follower_count property" do
+ test "it sets the follower_count property" do
user = insert(:user)
follower = insert(:user)
User.follow(follower, user)
- assert user.info.follower_count == 0
+ assert user.follower_count == 0
{:ok, user} = User.update_follower_count(user)
- assert user.info.follower_count == 1
+ assert user.follower_count == 1
end
end
describe "remove duplicates from following list" do
test "it removes duplicates" do
user = insert(:user)
follower = insert(:user)
{:ok, %User{following: following} = follower} = User.follow(follower, user)
assert length(following) == 2
{:ok, follower} =
follower
|> User.update_changeset(%{following: following ++ following})
|> Repo.update()
assert length(follower.following) == 4
{:ok, follower} = User.remove_duplicated_following(follower)
assert length(follower.following) == 2
end
test "it does nothing when following is uniq" do
user = insert(:user)
follower = insert(:user)
{:ok, follower} = User.follow(follower, user)
assert length(follower.following) == 2
{:ok, follower} = User.remove_duplicated_following(follower)
assert length(follower.following) == 2
end
end
describe "follow_import" do
test "it imports user followings from list" do
[user1, user2, user3] = insert_list(3, :user)
identifiers = [
user2.ap_id,
user3.nickname
]
{:ok, job} = User.follow_import(user1, identifiers)
result = ObanHelpers.perform(job)
assert is_list(result)
assert result == [user2, user3]
end
end
describe "mutes" do
test "it mutes people" do
user = insert(:user)
muted_user = insert(:user)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
{:ok, user} = User.mute(user, muted_user)
assert User.mutes?(user, muted_user)
assert User.muted_notifications?(user, muted_user)
end
test "it unmutes users" do
user = insert(:user)
muted_user = insert(:user)
{:ok, user} = User.mute(user, muted_user)
{:ok, user} = User.unmute(user, muted_user)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
end
test "it mutes user without notifications" do
user = insert(:user)
muted_user = insert(:user)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
{:ok, user} = User.mute(user, muted_user, false)
assert User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
end
end
describe "blocks" do
test "it blocks people" do
user = insert(:user)
blocked_user = insert(:user)
refute User.blocks?(user, blocked_user)
{:ok, user} = User.block(user, blocked_user)
assert User.blocks?(user, blocked_user)
end
test "it unblocks users" do
user = insert(:user)
blocked_user = insert(:user)
{:ok, user} = User.block(user, blocked_user)
{:ok, user} = User.unblock(user, blocked_user)
refute User.blocks?(user, blocked_user)
end
test "blocks tear down cyclical follow relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocker} = User.follow(blocker, blocked)
{:ok, blocked} = User.follow(blocked, blocker)
assert User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, blocker} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "blocks tear down blocker->blocked follow relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocker} = User.follow(blocker, blocked)
assert User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
{:ok, blocker} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "blocks tear down blocked->blocker follow relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocked} = User.follow(blocked, blocker)
refute User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, blocker} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "blocks tear down blocked->blocker subscription relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocker} = User.subscribe(blocked, blocker)
assert User.subscribed_to?(blocked, blocker)
refute User.subscribed_to?(blocker, blocked)
{:ok, blocker} = User.block(blocker, blocked)
assert User.blocks?(blocker, blocked)
refute User.subscribed_to?(blocker, blocked)
refute User.subscribed_to?(blocked, blocker)
end
end
describe "domain blocking" do
test "blocks domains" do
user = insert(:user)
collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
assert User.blocks?(user, collateral_user)
end
test "does not block domain with same end" do
user = insert(:user)
collateral_user =
insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
refute User.blocks?(user, collateral_user)
end
test "does not block domain with same end if wildcard added" do
user = insert(:user)
collateral_user =
insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com")
refute User.blocks?(user, collateral_user)
end
test "blocks domain with wildcard for subdomain" do
user = insert(:user)
user_from_subdomain =
insert(:user, %{ap_id: "https://subdomain.awful-and-rude-instance.com/user/bully"})
user_with_two_subdomains =
insert(:user, %{
ap_id: "https://subdomain.second_subdomain.awful-and-rude-instance.com/user/bully"
})
user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com")
assert User.blocks?(user, user_from_subdomain)
assert User.blocks?(user, user_with_two_subdomains)
assert User.blocks?(user, user_domain)
end
test "unblocks domains" do
user = insert(:user)
collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
{:ok, user} = User.unblock_domain(user, "awful-and-rude-instance.com")
refute User.blocks?(user, collateral_user)
end
end
describe "blocks_import" do
test "it imports user blocks from list" do
[user1, user2, user3] = insert_list(3, :user)
identifiers = [
user2.ap_id,
user3.nickname
]
{:ok, job} = User.blocks_import(user1, identifiers)
result = ObanHelpers.perform(job)
assert is_list(result)
assert result == [user2, user3]
end
end
test "get recipients from activity" do
actor = insert(:user)
user = insert(:user, local: true)
user_two = insert(:user, local: false)
addressed = insert(:user, local: true)
addressed_remote = insert(:user, local: false)
{:ok, activity} =
CommonAPI.post(actor, %{
"status" => "hey @#{addressed.nickname} @#{addressed_remote.nickname}"
})
assert Enum.map([actor, addressed], & &1.ap_id) --
Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == []
{:ok, user} = User.follow(user, actor)
{:ok, _user_two} = User.follow(user_two, actor)
recipients = User.get_recipients_from_activity(activity)
assert length(recipients) == 3
assert user in recipients
assert addressed in recipients
end
describe ".deactivate" do
test "can de-activate then re-activate a user" do
user = insert(:user)
- assert false == user.info.deactivated
+ assert false == user.deactivated
{:ok, user} = User.deactivate(user)
- assert true == user.info.deactivated
+ assert true == user.deactivated
{:ok, user} = User.deactivate(user, false)
- assert false == user.info.deactivated
+ assert false == user.deactivated
end
- test "hide a user from followers " do
+ test "hide a user from followers" do
user = insert(:user)
user2 = insert(:user)
{:ok, user} = User.follow(user, user2)
{:ok, _user} = User.deactivate(user)
info = User.get_cached_user_info(user2)
assert info.follower_count == 0
assert [] = User.get_followers(user2)
end
test "hide a user from friends" do
user = insert(:user)
user2 = insert(:user)
{:ok, user2} = User.follow(user2, user)
assert User.following_count(user2) == 1
{:ok, _user} = User.deactivate(user)
info = User.get_cached_user_info(user2)
assert info.following_count == 0
assert User.following_count(user2) == 0
assert [] = User.get_friends(user2)
end
test "hide a user's statuses from timelines and notifications" do
user = insert(:user)
user2 = insert(:user)
{:ok, user2} = User.follow(user2, user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}"})
activity = Repo.preload(activity, :bookmark)
[notification] = Pleroma.Notification.for_user(user2)
assert notification.activity.id == activity.id
assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark)
assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] ==
ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
{:ok, _user} = User.deactivate(user)
assert [] == ActivityPub.fetch_public_activities(%{})
assert [] == Pleroma.Notification.for_user(user2)
assert [] ==
ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
end
end
describe "delete" do
setup do
{:ok, user} = insert(:user) |> User.set_cache()
[user: user]
end
clear_config([:instance, :federating])
test ".delete_user_activities deletes all create activities", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
User.delete_user_activities(user)
# TODO: Remove favorites, repeats, delete activities.
refute Activity.get_by_id(activity.id)
end
test "it deletes deactivated user" do
- {:ok, user} = insert(:user, info: %{deactivated: true}) |> User.set_cache()
+ {:ok, user} = insert(:user, deactivated: true) |> User.set_cache()
{:ok, job} = User.delete(user)
{:ok, _user} = ObanHelpers.perform(job)
refute User.get_by_id(user.id)
end
test "it deletes a user, all follow relationships and all activities", %{user: user} do
follower = insert(:user)
{:ok, follower} = User.follow(follower, user)
object = insert(:note, user: user)
activity = insert(:note_activity, user: user, note: object)
object_two = insert(:note, user: follower)
activity_two = insert(:note_activity, user: follower, note: object_two)
{:ok, like, _} = CommonAPI.favorite(activity_two.id, user)
{:ok, like_two, _} = CommonAPI.favorite(activity.id, follower)
{:ok, repeat, _} = CommonAPI.repeat(activity_two.id, user)
{:ok, job} = User.delete(user)
{:ok, _user} = ObanHelpers.perform(job)
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, user)
refute User.get_by_id(user.id)
assert {:ok, nil} == Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
user_activities =
user.ap_id
|> Activity.Queries.by_actor()
|> Repo.all()
|> Enum.map(fn act -> act.data["type"] end)
assert Enum.all?(user_activities, fn act -> act in ~w(Delete Undo) end)
refute Activity.get_by_id(activity.id)
refute Activity.get_by_id(like.id)
refute Activity.get_by_id(like_two.id)
refute Activity.get_by_id(repeat.id)
end
test_with_mock "it sends out User Delete activity",
%{user: user},
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
Pleroma.Config.put([:instance, :federating], true)
{:ok, follower} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
{:ok, _} = User.follow(follower, user)
{:ok, job} = User.delete(user)
{:ok, _user} = ObanHelpers.perform(job)
assert ObanHelpers.member?(
%{
"op" => "publish_one",
"params" => %{
"inbox" => "http://mastodon.example.org/inbox",
"id" => "pleroma:fakeid"
}
},
all_enqueued(worker: Pleroma.Workers.PublisherWorker)
)
end
end
test "get_public_key_for_ap_id fetches a user that's not in the db" do
assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
end
describe "insert or update a user from given data" do
test "with normal data" do
user = insert(:user, %{nickname: "nick@name.de"})
data = %{ap_id: user.ap_id <> "xxx", name: user.name, nickname: user.nickname}
assert {:ok, %User{}} = User.insert_or_update_user(data)
end
test "with overly long fields" do
current_max_length = Pleroma.Config.get([:instance, :account_field_value_length], 255)
user = insert(:user, nickname: "nickname@supergood.domain")
data = %{
ap_id: user.ap_id,
name: user.name,
nickname: user.nickname,
- info: %{
- fields: [
- %{"name" => "myfield", "value" => String.duplicate("h", current_max_length + 1)}
- ]
- }
+ fields: [
+ %{"name" => "myfield", "value" => String.duplicate("h", current_max_length + 1)}
+ ]
}
assert {:ok, %User{}} = User.insert_or_update_user(data)
end
test "with an overly long bio" do
current_max_length = Pleroma.Config.get([:instance, :user_bio_length], 5000)
user = insert(:user, nickname: "nickname@supergood.domain")
data = %{
ap_id: user.ap_id,
name: user.name,
nickname: user.nickname,
bio: String.duplicate("h", current_max_length + 1),
info: %{}
}
assert {:ok, %User{}} = User.insert_or_update_user(data)
end
test "with an overly long display name" do
current_max_length = Pleroma.Config.get([:instance, :user_name_length], 100)
user = insert(:user, nickname: "nickname@supergood.domain")
data = %{
ap_id: user.ap_id,
name: String.duplicate("h", current_max_length + 1),
nickname: user.nickname,
info: %{}
}
assert {:ok, %User{}} = User.insert_or_update_user(data)
end
end
describe "per-user rich-text filtering" do
test "html_filter_policy returns default policies, when rich-text is enabled" do
user = insert(:user)
assert Pleroma.Config.get([:markup, :scrub_policy]) == User.html_filter_policy(user)
end
test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do
- user = insert(:user, %{info: %{no_rich_text: true}})
+ user = insert(:user, no_rich_text: true)
assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user)
end
end
describe "caching" do
test "invalidate_cache works" do
user = insert(:user)
_user_info = User.get_cached_user_info(user)
User.invalidate_cache(user)
{:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
{:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}")
{:ok, nil} = Cachex.get(:user_cache, "user_info:#{user.id}")
end
test "User.delete() plugs any possible zombie objects" do
user = insert(:user)
{:ok, job} = User.delete(user)
{:ok, _} = ObanHelpers.perform(job)
{:ok, cached_user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
assert cached_user != user
{:ok, cached_user} = Cachex.get(:user_cache, "nickname:#{user.ap_id}")
assert cached_user != user
end
end
test "auth_active?/1 works correctly" do
Pleroma.Config.put([:instance, :account_activation_required], true)
- local_user = insert(:user, local: true, info: %{confirmation_pending: true})
- confirmed_user = insert(:user, local: true, info: %{confirmation_pending: false})
+ local_user = insert(:user, local: true, confirmation_pending: true)
+ confirmed_user = insert(:user, local: true, confirmation_pending: false)
remote_user = insert(:user, local: false)
refute User.auth_active?(local_user)
assert User.auth_active?(confirmed_user)
assert User.auth_active?(remote_user)
end
describe "superuser?/1" do
test "returns false for unprivileged users" do
user = insert(:user, local: true)
refute User.superuser?(user)
end
test "returns false for remote users" do
user = insert(:user, local: false)
- remote_admin_user = insert(:user, local: false, info: %{is_admin: true})
+ remote_admin_user = insert(:user, local: false, is_admin: true)
refute User.superuser?(user)
refute User.superuser?(remote_admin_user)
end
test "returns true for local moderators" do
- user = insert(:user, local: true, info: %{is_moderator: true})
+ user = insert(:user, local: true, is_moderator: true)
assert User.superuser?(user)
end
test "returns true for local admins" do
- user = insert(:user, local: true, info: %{is_admin: true})
+ user = insert(:user, local: true, is_admin: true)
assert User.superuser?(user)
end
end
+ describe "invisible?/1" do
+ test "returns true for an invisible user" do
+ user = insert(:user, local: true, invisible: true)
+
+ assert User.invisible?(user)
+ end
+
+ test "returns false for a non-invisible user" do
+ user = insert(:user, local: true)
+
+ refute User.invisible?(user)
+ end
+ end
+
describe "visible_for?/2" do
test "returns true when the account is itself" do
user = insert(:user, local: true)
assert User.visible_for?(user, user)
end
test "returns false when the account is unauthenticated and auth is required" do
Pleroma.Config.put([:instance, :account_activation_required], true)
- user = insert(:user, local: true, info: %{confirmation_pending: true})
+ user = insert(:user, local: true, confirmation_pending: true)
other_user = insert(:user, local: true)
refute User.visible_for?(user, other_user)
end
test "returns true when the account is unauthenticated and auth is not required" do
- user = insert(:user, local: true, info: %{confirmation_pending: true})
+ user = insert(:user, local: true, confirmation_pending: true)
other_user = insert(:user, local: true)
assert User.visible_for?(user, other_user)
end
test "returns true when the account is unauthenticated and being viewed by a privileged account (auth required)" do
Pleroma.Config.put([:instance, :account_activation_required], true)
- user = insert(:user, local: true, info: %{confirmation_pending: true})
- other_user = insert(:user, local: true, info: %{is_admin: true})
+ user = insert(:user, local: true, confirmation_pending: true)
+ other_user = insert(:user, local: true, is_admin: true)
assert User.visible_for?(user, other_user)
end
end
describe "parse_bio/2" do
test "preserves hosts in user links text" do
remote_user = insert(:user, local: false, nickname: "nick@domain.com")
user = insert(:user)
bio = "A.k.a. @nick@domain.com"
expected_text =
~s(A.k.a. <span class="h-card"><a data-user="#{remote_user.id}" class="u-url mention" href="#{
remote_user.ap_id
}" rel="ugc">@<span>nick@domain.com</span></a></span>)
assert expected_text == User.parse_bio(bio, user)
end
test "Adds rel=me on linkbacked urls" do
user = insert(:user, ap_id: "https://social.example.org/users/lain")
bio = "http://example.com/rel_me/null"
expected_text = "<a href=\"#{bio}\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
bio = "http://example.com/rel_me/link"
expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
bio = "http://example.com/rel_me/anchor"
expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
end
end
test "follower count is updated when a follower is blocked" do
user = insert(:user)
follower = insert(:user)
follower2 = insert(:user)
follower3 = insert(:user)
{:ok, follower} = User.follow(follower, user)
{:ok, _follower2} = User.follow(follower2, user)
{:ok, _follower3} = User.follow(follower3, user)
{:ok, user} = User.block(user, follower)
assert User.user_info(user).follower_count == 2
end
describe "list_inactive_users_query/1" do
defp days_ago(days) do
NaiveDateTime.add(
NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
-days * 60 * 60 * 24,
:second
)
end
test "Users are inactive by default" do
total = 10
users =
Enum.map(1..total, fn _ ->
- insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false})
+ insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false)
end)
inactive_users_ids =
Pleroma.User.list_inactive_users_query()
|> Pleroma.Repo.all()
|> Enum.map(& &1.id)
Enum.each(users, fn user ->
assert user.id in inactive_users_ids
end)
end
test "Only includes users who has no recent activity" do
total = 10
users =
Enum.map(1..total, fn _ ->
- insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false})
+ insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false)
end)
{inactive, active} = Enum.split(users, trunc(total / 2))
Enum.map(active, fn user ->
to = Enum.random(users -- [user])
{:ok, _} =
CommonAPI.post(user, %{
"status" => "hey @#{to.nickname}"
})
end)
inactive_users_ids =
Pleroma.User.list_inactive_users_query()
|> Pleroma.Repo.all()
|> Enum.map(& &1.id)
Enum.each(active, fn user ->
refute user.id in inactive_users_ids
end)
Enum.each(inactive, fn user ->
assert user.id in inactive_users_ids
end)
end
test "Only includes users with no read notifications" do
total = 10
users =
Enum.map(1..total, fn _ ->
- insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false})
+ insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false)
end)
[sender | recipients] = users
{inactive, active} = Enum.split(recipients, trunc(total / 2))
Enum.each(recipients, fn to ->
{:ok, _} =
CommonAPI.post(sender, %{
"status" => "hey @#{to.nickname}"
})
{:ok, _} =
CommonAPI.post(sender, %{
"status" => "hey again @#{to.nickname}"
})
end)
Enum.each(active, fn user ->
[n1, _n2] = Pleroma.Notification.for_user(user)
{:ok, _} = Pleroma.Notification.read_one(user, n1.id)
end)
inactive_users_ids =
Pleroma.User.list_inactive_users_query()
|> Pleroma.Repo.all()
|> Enum.map(& &1.id)
Enum.each(active, fn user ->
refute user.id in inactive_users_ids
end)
Enum.each(inactive, fn user ->
assert user.id in inactive_users_ids
end)
end
end
describe "toggle_confirmation/1" do
test "if user is confirmed" do
- user = insert(:user, info: %{confirmation_pending: false})
+ user = insert(:user, confirmation_pending: false)
{:ok, user} = User.toggle_confirmation(user)
- assert user.info.confirmation_pending
- assert user.info.confirmation_token
+ assert user.confirmation_pending
+ assert user.confirmation_token
end
test "if user is unconfirmed" do
- user = insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"})
+ user = insert(:user, confirmation_pending: true, confirmation_token: "some token")
{:ok, user} = User.toggle_confirmation(user)
- refute user.info.confirmation_pending
- refute user.info.confirmation_token
+ refute user.confirmation_pending
+ refute user.confirmation_token
end
end
describe "ensure_keys_present" do
test "it creates keys for a user and stores them in info" do
user = insert(:user)
refute is_binary(user.keys)
{:ok, user} = User.ensure_keys_present(user)
assert is_binary(user.keys)
end
test "it doesn't create keys if there already are some" do
user = insert(:user, keys: "xxx")
{:ok, user} = User.ensure_keys_present(user)
assert user.keys == "xxx"
end
end
describe "get_ap_ids_by_nicknames" do
test "it returns a list of AP ids for a given set of nicknames" do
user = insert(:user)
user_two = insert(:user)
ap_ids = User.get_ap_ids_by_nicknames([user.nickname, user_two.nickname, "nonexistent"])
assert length(ap_ids) == 2
assert user.ap_id in ap_ids
assert user_two.ap_id in ap_ids
end
end
describe "sync followers count" do
setup do
user1 = insert(:user, local: false, ap_id: "http://localhost:4001/users/masto_closed")
user2 = insert(:user, local: false, ap_id: "http://localhost:4001/users/fuser2")
insert(:user, local: true)
- insert(:user, local: false, info: %{deactivated: true})
+ insert(:user, local: false, deactivated: true)
{:ok, user1: user1, user2: user2}
end
test "external_users/1 external active users with limit", %{user1: user1, user2: user2} do
[fdb_user1] = User.external_users(limit: 1)
assert fdb_user1.ap_id
assert fdb_user1.ap_id == user1.ap_id
assert fdb_user1.id == user1.id
[fdb_user2] = User.external_users(max_id: fdb_user1.id, limit: 1)
assert fdb_user2.ap_id
assert fdb_user2.ap_id == user2.ap_id
assert fdb_user2.id == user2.id
assert User.external_users(max_id: fdb_user2.id, limit: 1) == []
end
end
describe "set_info_cache/2" do
setup do
user = insert(:user)
{:ok, user: user}
end
test "update from args", %{user: user} do
User.set_info_cache(user, %{following_count: 15, follower_count: 18})
%{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
assert followers == 18
assert following == 15
end
test "without args", %{user: user} do
User.set_info_cache(user, %{})
%{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
assert followers == 0
assert following == 0
end
end
describe "user_info/2" do
setup do
user = insert(:user)
{:ok, user: user}
end
test "update from args", %{user: user} do
%{follower_count: followers, following_count: following} =
User.user_info(user, %{following_count: 15, follower_count: 18})
assert followers == 18
assert following == 15
end
test "without args", %{user: user} do
%{follower_count: followers, following_count: following} = User.user_info(user)
assert followers == 0
assert following == 0
end
end
describe "is_internal_user?/1" do
test "non-internal user returns false" do
user = insert(:user)
refute User.is_internal_user?(user)
end
test "user with no nickname returns true" do
user = insert(:user, %{nickname: nil})
assert User.is_internal_user?(user)
end
test "user with internal-prefixed nickname returns true" do
user = insert(:user, %{nickname: "internal.test"})
assert User.is_internal_user?(user)
end
end
describe "update_and_set_cache/1" do
test "returns error when user is stale instead Ecto.StaleEntryError" do
user = insert(:user)
changeset = Ecto.Changeset.change(user, bio: "test")
Repo.delete(user)
assert {:error, %Ecto.Changeset{errors: [id: {"is stale", [stale: true]}], valid?: false}} =
User.update_and_set_cache(changeset)
end
test "performs update cache if user updated" do
user = insert(:user)
assert {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
changeset = Ecto.Changeset.change(user, bio: "test-bio")
assert {:ok, %User{bio: "test-bio"} = user} = User.update_and_set_cache(changeset)
assert {:ok, user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id)
end
end
describe "following/followers synchronization" do
clear_config([:instance, :external_user_synchronization])
test "updates the counters normally on following/getting a follow when disabled" do
Pleroma.Config.put([:instance, :external_user_synchronization], false)
user = insert(:user)
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following",
- info: %{ap_enabled: true}
+ ap_enabled: true
)
assert User.user_info(other_user).following_count == 0
assert User.user_info(other_user).follower_count == 0
{:ok, user} = Pleroma.User.follow(user, other_user)
other_user = Pleroma.User.get_by_id(other_user.id)
assert User.user_info(user).following_count == 1
assert User.user_info(other_user).follower_count == 1
end
test "syncronizes the counters with the remote instance for the followed when enabled" do
Pleroma.Config.put([:instance, :external_user_synchronization], false)
user = insert(:user)
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following",
- info: %{ap_enabled: true}
+ ap_enabled: true
)
assert User.user_info(other_user).following_count == 0
assert User.user_info(other_user).follower_count == 0
Pleroma.Config.put([:instance, :external_user_synchronization], true)
{:ok, _user} = User.follow(user, other_user)
other_user = User.get_by_id(other_user.id)
assert User.user_info(other_user).follower_count == 437
end
test "syncronizes the counters with the remote instance for the follower when enabled" do
Pleroma.Config.put([:instance, :external_user_synchronization], false)
user = insert(:user)
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following",
- info: %{ap_enabled: true}
+ ap_enabled: true
)
assert User.user_info(other_user).following_count == 0
assert User.user_info(other_user).follower_count == 0
Pleroma.Config.put([:instance, :external_user_synchronization], true)
{:ok, other_user} = User.follow(other_user, user)
assert User.user_info(other_user).following_count == 152
end
end
describe "change_email/2" do
setup do
[user: insert(:user)]
end
test "blank email returns error", %{user: user} do
assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, "")
assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, nil)
end
test "non unique email returns error", %{user: user} do
%{email: email} = insert(:user)
assert {:error, %{errors: [email: {"has already been taken", _}]}} =
User.change_email(user, email)
end
test "invalid email returns error", %{user: user} do
assert {:error, %{errors: [email: {"has invalid format", _}]}} =
User.change_email(user, "cofe")
end
test "changes email", %{user: user} do
assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party")
end
end
- describe "set_password_reset_pending/2" do
- setup do
- [user: insert(:user)]
- end
-
- test "sets password_reset_pending to true", %{user: user} do
- %{password_reset_pending: password_reset_pending} = user.info
-
- refute password_reset_pending
-
- {:ok, %{info: %{password_reset_pending: password_reset_pending}}} =
- User.force_password_reset(user)
-
- assert password_reset_pending
- end
- end
-
- test "change_info/2" do
- user = insert(:user)
- assert user.info.hide_follows == false
-
- changeset = User.change_info(user, &User.Info.profile_update(&1, %{hide_follows: true}))
- assert changeset.changes.info.changes.hide_follows == true
- end
-
- test "update_info/2" do
- user = insert(:user)
- assert user.info.hide_follows == false
-
- assert {:ok, _} = User.update_info(user, &User.Info.profile_update(&1, %{hide_follows: true}))
-
- assert %{info: %{hide_follows: true}} = Repo.get(User, user.id)
- assert {:ok, %{info: %{hide_follows: true}}} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
- end
-
describe "get_cached_by_nickname_or_id" do
setup do
limit_to_local_content = Pleroma.Config.get([:instance, :limit_to_local_content])
local_user = insert(:user)
remote_user = insert(:user, nickname: "nickname@example.com", local: false)
on_exit(fn ->
Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local_content)
end)
[local_user: local_user, remote_user: remote_user]
end
test "allows getting remote users by id no matter what :limit_to_local_content is set to", %{
remote_user: remote_user
} do
Pleroma.Config.put([:instance, :limit_to_local_content], false)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
Pleroma.Config.put([:instance, :limit_to_local_content], true)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
end
test "disallows getting remote users by nickname without authentication when :limit_to_local_content is set to :unauthenticated",
%{remote_user: remote_user} do
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname)
end
test "allows getting remote users by nickname with authentication when :limit_to_local_content is set to :unauthenticated",
%{remote_user: remote_user, local_user: local_user} do
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.nickname, for: local_user)
end
test "disallows getting remote users by nickname when :limit_to_local_content is set to true",
%{remote_user: remote_user} do
Pleroma.Config.put([:instance, :limit_to_local_content], true)
assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname)
end
test "allows getting local users by nickname no matter what :limit_to_local_content is set to",
%{local_user: local_user} do
Pleroma.Config.put([:instance, :limit_to_local_content], false)
assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
Pleroma.Config.put([:instance, :limit_to_local_content], true)
assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
end
end
+
+ describe "update_email_notifications/2" do
+ setup do
+ user = insert(:user, email_notifications: %{"digest" => true})
+
+ {:ok, user: user}
+ end
+
+ test "Notifications are updated", %{user: user} do
+ true = user.email_notifications["digest"]
+ assert {:ok, result} = User.update_email_notifications(user, %{"digest" => false})
+ assert result.email_notifications["digest"] == false
+ end
+ end
end
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index 6a3e48b5e..a5414c521 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -1,956 +1,1037 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
alias Pleroma.Activity
alias Pleroma.Delivery
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.ReceiverWorker
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
clear_config_all([:instance, :federating],
do: Pleroma.Config.put([:instance, :federating], true)
)
describe "/relay" do
clear_config([:instance, :allow_relay])
test "with the relay active, it returns the relay user", %{conn: conn} do
res =
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(200)
assert res["id"] =~ "/relay"
end
test "with the relay disabled, it returns 404", %{conn: conn} do
Pleroma.Config.put([:instance, :allow_relay], false)
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
|> assert
end
end
describe "/internal/fetch" do
test "it returns the internal fetch user", %{conn: conn} do
res =
conn
|> get(activity_pub_path(conn, :internal_fetch))
|> json_response(200)
assert res["id"] =~ "/fetch"
end
end
describe "/users/:nickname" do
test "it returns a json representation of the user with accept application/json", %{
conn: conn
} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/#{user.nickname}")
user = User.get_cached_by_id(user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
test "it returns a json representation of the user with accept application/activity+json", %{
conn: conn
} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}")
user = User.get_cached_by_id(user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
test "it returns a json representation of the user with accept application/ld+json", %{
conn: conn
} do
user = insert(:user)
conn =
conn
|> put_req_header(
"accept",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
|> get("/users/#{user.nickname}")
user = User.get_cached_by_id(user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
end
describe "/object/:uuid" do
test "it returns a json representation of the object with accept application/json", %{
conn: conn
} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
test "it returns a json representation of the object with accept application/activity+json",
%{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
test "it returns a json representation of the object with accept application/ld+json", %{
conn: conn
} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header(
"accept",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
test "it returns 404 for non-public messages", %{conn: conn} do
note = insert(:direct_note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 404)
end
test "it returns 404 for tombstone objects", %{conn: conn} do
tombstone = insert(:tombstone)
uuid = String.split(tombstone.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 404)
end
test "it caches a response", %{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok) == json_response(conn2, :ok)
assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
end
test "cached purged after object deletion", %{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
Object.delete(note)
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert "Not found" == json_response(conn2, :not_found)
end
end
describe "/activities/:uuid" do
test "it returns a json representation of the activity", %{conn: conn} do
activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
end
test "it returns 404 for non-public activities", %{conn: conn} do
activity = insert(:direct_note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn, 404)
end
test "it caches a response", %{conn: conn} do
activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok) == json_response(conn2, :ok)
assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
end
test "cached purged after activity deletion", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "cofe"})
uuid = String.split(activity.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
Activity.delete_by_ap_id(activity.object.data["id"])
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert "Not found" == json_response(conn2, :not_found)
end
end
describe "/inbox" do
test "it inserts an incoming activity into the database", %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it clears `unreachable` federation status of the sender", %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
sender_url = data["actor"]
Instances.set_consistently_unreachable(sender_url)
refute Instances.reachable?(sender_url)
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
assert Instances.reachable?(sender_url)
end
end
describe "/users/:nickname/inbox" do
setup do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
[data: data]
end
test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
user = insert(:user)
data = Map.put(data, "bcc", [user.ap_id])
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
+ test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
+ user = insert(:user)
+
+ data =
+ Map.put(data, "to", user.ap_id)
+ |> Map.delete("cc")
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ assert Activity.get_by_ap_id(data["id"])
+ end
+
+ test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
+ user = insert(:user)
+
+ data =
+ Map.put(data, "cc", user.ap_id)
+ |> Map.delete("to")
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ %Activity{} = activity = Activity.get_by_ap_id(data["id"])
+ assert user.ap_id in activity.recipients
+ end
+
+ test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
+ user = insert(:user)
+
+ data =
+ Map.put(data, "bcc", user.ap_id)
+ |> Map.delete("to")
+ |> Map.delete("cc")
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ assert Activity.get_by_ap_id(data["id"])
+ end
+
+ test "it accepts announces with to as string instead of array", %{conn: conn} do
+ user = insert(:user)
+
+ data = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "actor" => "http://mastodon.example.org/users/admin",
+ "id" => "http://mastodon.example.org/users/admin/statuses/19512778738411822/activity",
+ "object" => "https://mastodon.social/users/emelie/statuses/101849165031453009",
+ "to" => "https://www.w3.org/ns/activitystreams#Public",
+ "cc" => [user.ap_id],
+ "type" => "Announce"
+ }
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+ ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
+ %Activity{} = activity = Activity.get_by_ap_id(data["id"])
+ assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
+ end
+
test "it accepts messages from actors that are followed by the user", %{
conn: conn,
data: data
} do
recipient = insert(:user)
actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
{:ok, recipient} = User.follow(recipient, actor)
object =
data["object"]
|> Map.put("attributedTo", actor.ap_id)
data =
data
|> Map.put("actor", actor.ap_id)
|> Map.put("object", object)
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it rejects reads from other users", %{conn: conn} do
user = insert(:user)
otheruser = insert(:user)
conn =
conn
|> assign(:user, otheruser)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox")
assert json_response(conn, 403)
end
test "it doesn't crash without an authenticated user", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox")
assert json_response(conn, 403)
end
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:direct_note_activity)
note_object = Object.normalize(note_activity)
user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
conn =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox?page=true")
assert response(conn, 200) =~ note_object.data["content"]
end
test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
user = insert(:user)
data = Map.put(data, "bcc", [user.ap_id])
sender_host = URI.parse(data["actor"]).host
Instances.set_consistently_unreachable(sender_host)
refute Instances.reachable?(sender_host)
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
assert Instances.reachable?(sender_host)
end
test "it removes all follower collections but actor's", %{conn: conn} do
[actor, recipient] = insert_pair(:user)
data =
File.read!("test/fixtures/activitypub-client-post-activity.json")
|> Poison.decode!()
object = Map.put(data["object"], "attributedTo", actor.ap_id)
data =
data
|> Map.put("id", Utils.generate_object_id())
|> Map.put("actor", actor.ap_id)
|> Map.put("object", object)
|> Map.put("cc", [
recipient.follower_address,
actor.follower_address
])
|> Map.put("to", [
recipient.ap_id,
recipient.follower_address,
"https://www.w3.org/ns/activitystreams#Public"
])
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
activity = Activity.get_by_ap_id(data["id"])
assert activity.id
assert actor.follower_address in activity.recipients
assert actor.follower_address in activity.data["cc"]
refute recipient.follower_address in activity.recipients
refute recipient.follower_address in activity.data["cc"]
refute recipient.follower_address in activity.data["to"]
end
end
describe "/users/:nickname/outbox" do
test "it will not bomb when there is no activity", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox")
result = json_response(conn, 200)
assert user.ap_id <> "/outbox" == result["id"]
end
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ note_object.data["content"]
end
test "it returns an announce activity in a collection", %{conn: conn} do
announce_activity = insert(:announce_activity)
user = User.get_cached_by_ap_id(announce_activity.data["actor"])
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ announce_activity.data["object"]
end
test "it rejects posts from other users", %{conn: conn} do
data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
user = insert(:user)
otheruser = insert(:user)
conn =
conn
|> assign(:user, otheruser)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it inserts an incoming create activity into the database", %{conn: conn} do
data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
result = json_response(conn, 201)
assert Activity.get_by_ap_id(result["id"])
end
test "it rejects an incoming activity with bogus type", %{conn: conn} do
data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
user = insert(:user)
data =
data
|> Map.put("type", "BadType")
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
data = %{
type: "Delete",
object: %{
id: note_object.data["id"]
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
result = json_response(conn, 201)
assert Activity.get_by_ap_id(result["id"])
assert object = Object.get_by_ap_id(note_object.data["id"])
assert object.data["type"] == "Tombstone"
end
test "it rejects delete activity of object from other actor", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity)
user = insert(:user)
data = %{
type: "Delete",
object: %{
id: note_object.data["id"]
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it increases like count when receiving a like action", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
data = %{
type: "Like",
object: %{
id: note_object.data["id"]
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
result = json_response(conn, 201)
assert Activity.get_by_ap_id(result["id"])
assert object = Object.get_by_ap_id(note_object.data["id"])
assert object.data["like_count"] == 1
end
end
describe "/relay/followers" do
test "it returns relay followers", %{conn: conn} do
relay_actor = Relay.get_actor()
user = insert(:user)
User.follow(user, relay_actor)
result =
conn
|> assign(:relay, true)
|> get("/relay/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == [user.ap_id]
end
end
describe "/relay/following" do
test "it returns relay following", %{conn: conn} do
result =
conn
|> assign(:relay, true)
|> get("/relay/following")
|> json_response(200)
assert result["first"]["orderedItems"] == []
end
end
describe "/users/:nickname/followers" do
test "it returns the followers in a collection", %{conn: conn} do
user = insert(:user)
user_two = insert(:user)
User.follow(user, user_two)
result =
conn
|> get("/users/#{user_two.nickname}/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == [user.ap_id]
end
test "it returns returns a uri if the user has 'hide_followers' set", %{conn: conn} do
user = insert(:user)
- user_two = insert(:user, %{info: %{hide_followers: true}})
+ user_two = insert(:user, hide_followers: true)
User.follow(user, user_two)
result =
conn
|> get("/users/#{user_two.nickname}/followers")
|> json_response(200)
assert is_binary(result["first"])
end
test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is not authenticated",
%{conn: conn} do
- user = insert(:user, %{info: %{hide_followers: true}})
+ user = insert(:user, hide_followers: true)
result =
conn
|> get("/users/#{user.nickname}/followers?page=1")
assert result.status == 403
assert result.resp_body == ""
end
test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
%{conn: conn} do
- user = insert(:user, %{info: %{hide_followers: true}})
+ user = insert(:user, hide_followers: true)
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/followers?page=1")
|> json_response(200)
assert result["totalItems"] == 1
assert result["orderedItems"] == [other_user.ap_id]
end
test "it works for more than 10 users", %{conn: conn} do
user = insert(:user)
Enum.each(1..15, fn _ ->
other_user = insert(:user)
User.follow(other_user, user)
end)
result =
conn
|> get("/users/#{user.nickname}/followers")
|> json_response(200)
assert length(result["first"]["orderedItems"]) == 10
assert result["first"]["totalItems"] == 15
assert result["totalItems"] == 15
result =
conn
|> get("/users/#{user.nickname}/followers?page=2")
|> json_response(200)
assert length(result["orderedItems"]) == 5
assert result["totalItems"] == 15
end
end
describe "/users/:nickname/following" do
test "it returns the following in a collection", %{conn: conn} do
user = insert(:user)
user_two = insert(:user)
User.follow(user, user_two)
result =
conn
|> get("/users/#{user.nickname}/following")
|> json_response(200)
assert result["first"]["orderedItems"] == [user_two.ap_id]
end
test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
- user = insert(:user, %{info: %{hide_follows: true}})
+ user = insert(:user, hide_follows: true)
user_two = insert(:user)
User.follow(user, user_two)
result =
conn
|> get("/users/#{user.nickname}/following")
|> json_response(200)
assert is_binary(result["first"])
end
test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is not authenticated",
%{conn: conn} do
- user = insert(:user, %{info: %{hide_follows: true}})
+ user = insert(:user, hide_follows: true)
result =
conn
|> get("/users/#{user.nickname}/following?page=1")
assert result.status == 403
assert result.resp_body == ""
end
test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
%{conn: conn} do
- user = insert(:user, %{info: %{hide_follows: true}})
+ user = insert(:user, hide_follows: true)
other_user = insert(:user)
{:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/following?page=1")
|> json_response(200)
assert result["totalItems"] == 1
assert result["orderedItems"] == [other_user.ap_id]
end
test "it works for more than 10 users", %{conn: conn} do
user = insert(:user)
Enum.each(1..15, fn _ ->
user = User.get_cached_by_id(user.id)
other_user = insert(:user)
User.follow(user, other_user)
end)
result =
conn
|> get("/users/#{user.nickname}/following")
|> json_response(200)
assert length(result["first"]["orderedItems"]) == 10
assert result["first"]["totalItems"] == 15
assert result["totalItems"] == 15
result =
conn
|> get("/users/#{user.nickname}/following?page=2")
|> json_response(200)
assert length(result["orderedItems"]) == 5
assert result["totalItems"] == 15
end
end
describe "delivery tracking" do
test "it tracks a signed object fetch", %{conn: conn} do
user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity)
object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(object_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
end
test "it tracks a signed activity fetch", %{conn: conn} do
user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity)
activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(activity_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
end
test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
user = insert(:user, local: false)
other_user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity)
object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(object_path)
|> json_response(200)
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, other_user)
|> get(object_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
assert Delivery.get(object.id, other_user.id)
end
test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
user = insert(:user, local: false)
other_user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity)
activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(activity_path)
|> json_response(200)
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, other_user)
|> get(activity_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
assert Delivery.get(object.id, other_user.id)
end
end
describe "Additionnal ActivityPub C2S endpoints" do
test "/api/ap/whoami", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> get("/api/ap/whoami")
user = User.get_cached_by_id(user.id)
assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
end
clear_config([:media_proxy])
clear_config([Pleroma.Upload])
test "uploadMedia", %{conn: conn} do
user = insert(:user)
desc = "Description of the image"
image = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
conn =
conn
|> assign(:user, user)
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
assert object = json_response(conn, :created)
assert object["name"] == desc
assert object["type"] == "Document"
assert object["actor"] == user.ap_id
end
end
end
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index ddc6493c0..e12516722 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -1,1461 +1,1488 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Builders.ActivityBuilder
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
import Tesla.Mock
import Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
clear_config([:instance, :federating])
describe "streaming out participations" do
test "it streams them out" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity)
participations =
conversation.participations
|> Repo.preload(:user)
with_mock Pleroma.Web.Streamer,
stream: fn _, _ -> nil end do
ActivityPub.stream_out_participations(conversation.participations)
assert called(Pleroma.Web.Streamer.stream("participation", participations))
end
end
+
+ test "streams them out on activity creation" do
+ user_one = insert(:user)
+ user_two = insert(:user)
+
+ with_mock Pleroma.Web.Streamer,
+ stream: fn _, _ -> nil end do
+ {:ok, activity} =
+ CommonAPI.post(user_one, %{
+ "status" => "@#{user_two.nickname}",
+ "visibility" => "direct"
+ })
+
+ conversation =
+ activity.data["context"]
+ |> Pleroma.Conversation.get_for_ap_id()
+ |> Repo.preload(participations: :user)
+
+ assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations))
+ end
+ end
end
describe "fetching restricted by visibility" do
test "it restricts by the appropriate visibility" do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
activities =
ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id})
assert activities == [direct_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id})
assert activities == [unlisted_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id})
assert activities == [private_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id})
assert activities == [public_activity]
activities =
ActivityPub.fetch_activities([], %{
:visibility => ~w[private public],
"actor_id" => user.ap_id
})
assert activities == [public_activity, private_activity]
end
end
describe "fetching excluded by visibility" do
test "it excludes by the appropriate visibility" do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "direct",
"actor_id" => user.ap_id
})
assert public_activity in activities
assert unlisted_activity in activities
assert private_activity in activities
refute direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "unlisted",
"actor_id" => user.ap_id
})
assert public_activity in activities
refute unlisted_activity in activities
assert private_activity in activities
assert direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "private",
"actor_id" => user.ap_id
})
assert public_activity in activities
assert unlisted_activity in activities
refute private_activity in activities
assert direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "public",
"actor_id" => user.ap_id
})
refute public_activity in activities
assert unlisted_activity in activities
assert private_activity in activities
assert direct_activity in activities
end
end
describe "building a user from his ap id" do
test "it returns a user" do
user_id = "http://mastodon.example.org/users/admin"
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert user.ap_id == user_id
assert user.nickname == "admin@mastodon.example.org"
- assert user.info.source_data
- assert user.info.ap_enabled
+ assert user.source_data
+ assert user.ap_enabled
assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
end
+ test "it returns a user that is invisible" do
+ user_id = "http://mastodon.example.org/users/relay"
+ {:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
+ assert User.invisible?(user)
+ end
+
test "it fetches the appropriate tag-restricted posts" do
user = insert(:user)
{:ok, status_one} = CommonAPI.post(user, %{"status" => ". #test"})
{:ok, status_two} = CommonAPI.post(user, %{"status" => ". #essais"})
{:ok, status_three} = CommonAPI.post(user, %{"status" => ". #test #reject"})
fetch_one = ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => "test"})
fetch_two =
ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => ["test", "essais"]})
fetch_three =
ActivityPub.fetch_activities([], %{
"type" => "Create",
"tag" => ["test", "essais"],
"tag_reject" => ["reject"]
})
fetch_four =
ActivityPub.fetch_activities([], %{
"type" => "Create",
"tag" => ["test"],
"tag_all" => ["test", "reject"]
})
assert fetch_one == [status_one, status_three]
assert fetch_two == [status_one, status_two, status_three]
assert fetch_three == [status_one, status_two]
assert fetch_four == [status_three]
end
end
describe "insertion" do
test "drops activities beyond a certain limit" do
limit = Pleroma.Config.get([:instance, :remote_limit])
random_text =
:crypto.strong_rand_bytes(limit + 1)
|> Base.encode64()
|> binary_part(0, limit + 1)
data = %{
"ok" => true,
"object" => %{
"content" => random_text
}
}
assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data)
end
test "doesn't drop activities with content being null" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => nil
}
}
assert {:ok, _} = ActivityPub.insert(data)
end
test "returns the activity if one with the same id is already in" do
activity = insert(:note_activity)
{:ok, new_activity} = ActivityPub.insert(activity.data)
assert activity.id == new_activity.id
end
test "inserts a given map into the activity database, giving it an id if it has none." do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert activity.data["ok"] == data["ok"]
assert is_binary(activity.data["id"])
given_id = "bla"
data = %{
"id" => given_id,
"actor" => user.ap_id,
"to" => [],
"context" => "blabla",
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert activity.data["ok"] == data["ok"]
assert activity.data["id"] == given_id
assert activity.data["context"] == "blabla"
assert activity.data["context_id"]
end
test "adds a context when none is there" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
object = Pleroma.Object.normalize(activity)
assert is_binary(activity.data["context"])
assert is_binary(object.data["context"])
assert activity.data["context_id"]
assert object.data["context_id"]
end
test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert object = Object.normalize(activity)
assert is_binary(object.data["id"])
end
end
describe "listen activities" do
test "does not increase user note count" do
user = insert(:user)
{:ok, activity} =
ActivityPub.listen(%{
to: ["https://www.w3.org/ns/activitystreams#Public"],
actor: user,
context: "",
object: %{
"actor" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"artist" => "lain",
"title" => "lain radio episode 1",
"length" => 180_000,
"type" => "Audio"
}
})
assert activity.actor == user.ap_id
user = User.get_cached_by_id(user.id)
- assert user.info.note_count == 0
+ assert user.note_count == 0
end
test "can be fetched into a timeline" do
_listen_activity_1 = insert(:listen)
_listen_activity_2 = insert(:listen)
_listen_activity_3 = insert(:listen)
timeline = ActivityPub.fetch_activities([], %{"type" => ["Listen"]})
assert length(timeline) == 3
end
end
describe "create activities" do
test "removes doubled 'to' recipients" do
user = insert(:user)
{:ok, activity} =
ActivityPub.create(%{
to: ["user1", "user1", "user2"],
actor: user,
context: "",
object: %{
"to" => ["user1", "user1", "user2"],
"type" => "Note",
"content" => "testing"
}
})
assert activity.data["to"] == ["user1", "user2"]
assert activity.actor == user.ap_id
assert activity.recipients == ["user1", "user2", user.ap_id]
end
test "increases user note count only for public activities" do
user = insert(:user)
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "1",
"visibility" => "public"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "2",
"visibility" => "unlisted"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "2",
"visibility" => "private"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "3",
"visibility" => "direct"
})
user = User.get_cached_by_id(user.id)
- assert user.info.note_count == 2
+ assert user.note_count == 2
end
test "increases replies count" do
user = insert(:user)
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
ap_id = activity.data["id"]
reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
# public
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 1
# unlisted
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
# private
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
# direct
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
end
end
describe "fetch activities for recipients" do
test "retrieve the activities for certain recipients" do
{:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]})
{:ok, activity_two} = ActivityBuilder.insert(%{"to" => ["someone_else"]})
{:ok, _activity_three} = ActivityBuilder.insert(%{"to" => ["noone"]})
activities = ActivityPub.fetch_activities(["someone", "someone_else"])
assert length(activities) == 2
assert activities == [activity_one, activity_two]
end
end
describe "fetch activities in context" do
test "retrieves activities that have a given context" do
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
{:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"})
activity_five = insert(:note_activity)
user = insert(:user)
{:ok, user} = User.block(user, %{ap_id: activity_five.data["actor"]})
activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user})
assert activities == [activity_two, activity]
end
end
test "doesn't return blocked activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
{:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]})
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => nil, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
end
test "doesn't return transitive interactions concerning blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
{:ok, blocker} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
{:ok, activity_two} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})
{:ok, activity_three} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
{:ok, activity_four} = CommonAPI.post(blockee, %{"status" => "hey! @#{blocker.nickname}"})
activities = ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
assert Enum.member?(activities, activity_one)
refute Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_four)
end
test "doesn't return announce activities concerning blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
{:ok, blocker} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
{:ok, activity_two} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
{:ok, activity_three, _} = CommonAPI.repeat(activity_two.id, friend)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
|> Enum.map(fn act -> act.id end)
assert Enum.member?(activities, activity_one.id)
refute Enum.member?(activities, activity_two.id)
refute Enum.member?(activities, activity_three.id)
end
test "doesn't return activities from blocked domains" do
domain = "dogwhistle.zone"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
activity = insert(:note_activity, %{note: note})
user = insert(:user)
{:ok, user} = User.block_domain(user, domain)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute activity in activities
followed_user = insert(:user)
ActivityPub.follow(user, followed_user)
{:ok, repeat_activity, _} = CommonAPI.repeat(activity.id, followed_user)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute repeat_activity in activities
end
test "doesn't return muted activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
# Calling with 'with_muted' will deliver muted activities, too.
activities =
ActivityPub.fetch_activities([], %{
"muting_user" => user,
"with_muted" => true,
"skip_preload" => true
})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]})
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
activities = ActivityPub.fetch_activities([], %{"muting_user" => nil, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
end
test "doesn't return thread muted activities" do
user = insert(:user)
_activity_one = insert(:note_activity)
note_two = insert(:note, data: %{"context" => "suya.."})
activity_two = insert(:note_activity, note: note_two)
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user})
end
test "returns thread muted activities when with_muted is set" do
user = insert(:user)
_activity_one = insert(:note_activity)
note_two = insert(:note, data: %{"context" => "suya.."})
activity_two = insert(:note_activity, note: note_two)
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
assert [_activity_two, _activity_one] =
ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true})
end
test "does include announces on request" do
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.follow(user, booster)
{:ok, announce, _object} = CommonAPI.repeat(activity_three.id, booster)
[announce_activity] = ActivityPub.fetch_activities([user.ap_id | user.following])
assert announce_activity.id == announce.id
end
test "excludes reblogs on request" do
user = insert(:user)
{:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
{:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
[activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"})
assert activity == expected_activity
end
describe "public fetch activities" do
test "doesn't retrieve unlisted activities" do
user = insert(:user)
{:ok, _unlisted_activity} =
CommonAPI.post(user, %{"status" => "yeah", "visibility" => "unlisted"})
{:ok, listed_activity} = CommonAPI.post(user, %{"status" => "yeah"})
[activity] = ActivityPub.fetch_public_activities()
assert activity == listed_activity
end
test "retrieves public activities" do
_activities = ActivityPub.fetch_public_activities()
%{public: public} = ActivityBuilder.public_and_non_public()
activities = ActivityPub.fetch_public_activities()
assert length(activities) == 1
assert Enum.at(activities, 0) == public
end
test "retrieves a maximum of 20 activities" do
activities = ActivityBuilder.insert_list(30)
last_expected = List.last(activities)
activities = ActivityPub.fetch_public_activities()
last = List.last(activities)
assert length(activities) == 20
assert last == last_expected
end
test "retrieves ids starting from a since_id" do
activities = ActivityBuilder.insert_list(30)
later_activities = ActivityBuilder.insert_list(10)
since_id = List.last(activities).id
last_expected = List.last(later_activities)
activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id})
last = List.last(activities)
assert length(activities) == 10
assert last == last_expected
end
test "retrieves ids up to max_id" do
_first_activities = ActivityBuilder.insert_list(10)
activities = ActivityBuilder.insert_list(20)
later_activities = ActivityBuilder.insert_list(10)
max_id = List.first(later_activities).id
last_expected = List.last(activities)
activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id})
last = List.last(activities)
assert length(activities) == 20
assert last == last_expected
end
test "paginates via offset/limit" do
_first_activities = ActivityBuilder.insert_list(10)
activities = ActivityBuilder.insert_list(10)
_later_activities = ActivityBuilder.insert_list(10)
first_expected = List.first(activities)
activities =
ActivityPub.fetch_public_activities(%{"page" => "2", "page_size" => "20"}, :offset)
first = List.first(activities)
assert length(activities) == 20
assert first == first_expected
end
test "doesn't return reblogs for users for whom reblogs have been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = CommonAPI.hide_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
refute Enum.any?(activities, fn %{id: id} -> id == activity.id end)
end
test "returns reblogs for users for whom reblogs have not been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = CommonAPI.hide_reblogs(user, booster)
{:ok, user} = CommonAPI.show_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
assert Enum.any?(activities, fn %{id: id} -> id == activity.id end)
end
end
describe "like an object" do
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity)
user = insert(:user)
{:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
assert called(Pleroma.Web.Federator.publish(like_activity))
end
test "returns exist activity if object already liked" do
note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity)
user = insert(:user)
{:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
{:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity)
assert like_activity == like_activity_exist
end
test "adds a like activity to the db" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity)
user = insert(:user)
user_two = insert(:user)
{:ok, like_activity, object} = ActivityPub.like(user, object)
assert like_activity.data["actor"] == user.ap_id
assert like_activity.data["type"] == "Like"
assert like_activity.data["object"] == object.data["id"]
assert like_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
assert like_activity.data["context"] == object.data["context"]
assert object.data["like_count"] == 1
assert object.data["likes"] == [user.ap_id]
# Just return the original activity if the user already liked it.
{:ok, same_like_activity, object} = ActivityPub.like(user, object)
assert like_activity == same_like_activity
assert object.data["likes"] == [user.ap_id]
assert object.data["like_count"] == 1
{:ok, _like_activity, object} = ActivityPub.like(user_two, object)
assert object.data["like_count"] == 2
end
end
describe "unliking" do
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, object} = ActivityPub.unlike(user, object)
refute called(Pleroma.Web.Federator.publish())
{:ok, _like_activity, object} = ActivityPub.like(user, object)
assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert called(Pleroma.Web.Federator.publish(unlike_activity))
end
test "unliking a previously liked object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
# Unliking something that hasn't been liked does nothing
{:ok, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
{:ok, like_activity, object} = ActivityPub.like(user, object)
assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert Activity.get_by_id(like_activity.id) == nil
assert note_activity.actor in unlike_activity.recipients
end
end
describe "announcing an object" do
test "adds an announce activity to the db" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
assert object.data["announcements"] == [user.ap_id]
assert announce_activity.data["to"] == [
User.ap_followers(user),
note_activity.data["actor"]
]
assert announce_activity.data["object"] == object.data["id"]
assert announce_activity.data["actor"] == user.ap_id
assert announce_activity.data["context"] == object.data["context"]
end
end
describe "announcing a private object" do
test "adds an announce activity to the db if the audience is not widened" do
user = insert(:user)
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
object = Object.normalize(note_activity)
{:ok, announce_activity, object} = ActivityPub.announce(user, object, nil, true, false)
assert announce_activity.data["to"] == [User.ap_followers(user)]
assert announce_activity.data["object"] == object.data["id"]
assert announce_activity.data["actor"] == user.ap_id
assert announce_activity.data["context"] == object.data["context"]
end
test "does not add an announce activity to the db if the audience is widened" do
user = insert(:user)
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
object = Object.normalize(note_activity)
assert {:error, _} = ActivityPub.announce(user, object, nil, true, true)
end
test "does not add an announce activity to the db if the announcer is not the author" do
user = insert(:user)
announcer = insert(:user)
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
object = Object.normalize(note_activity)
assert {:error, _} = ActivityPub.announce(announcer, object, nil, true, false)
end
end
describe "unannouncing an object" do
test "unannouncing a previously announced object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
# Unannouncing an object that is not announced does nothing
# {:ok, object} = ActivityPub.unannounce(user, object)
# assert object.data["announcement_count"] == 0
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
{:ok, unannounce_activity, object} = ActivityPub.unannounce(user, object)
assert object.data["announcement_count"] == 0
assert unannounce_activity.data["to"] == [
User.ap_followers(user),
object.data["actor"]
]
assert unannounce_activity.data["type"] == "Undo"
assert unannounce_activity.data["object"] == announce_activity.data
assert unannounce_activity.data["actor"] == user.ap_id
assert unannounce_activity.data["context"] == announce_activity.data["context"]
assert Activity.get_by_id(announce_activity.id) == nil
end
end
describe "uploading files" do
test "copies the file to the configured folder" do
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, %Object{} = object} = ActivityPub.upload(file)
assert object.data["name"] == "an_image.jpg"
end
test "works with base64 encoded images" do
file = %{
"img" => data_uri()
}
{:ok, %Object{}} = ActivityPub.upload(file)
end
end
describe "fetch the latest Follow" do
test "fetches the latest Follow activity" do
%Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
follower = Repo.get_by(User, ap_id: activity.data["actor"])
followed = Repo.get_by(User, ap_id: activity.data["object"])
assert activity == Utils.fetch_latest_follow(follower, followed)
end
end
describe "following / unfollowing" do
test "creates a follow activity" do
follower = insert(:user)
followed = insert(:user)
{:ok, activity} = ActivityPub.follow(follower, followed)
assert activity.data["type"] == "Follow"
assert activity.data["actor"] == follower.ap_id
assert activity.data["object"] == followed.ap_id
end
test "creates an undo activity for the last follow" do
follower = insert(:user)
followed = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
{:ok, activity} = ActivityPub.unfollow(follower, followed)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == follower.ap_id
embedded_object = activity.data["object"]
assert is_map(embedded_object)
assert embedded_object["type"] == "Follow"
assert embedded_object["object"] == followed.ap_id
assert embedded_object["id"] == follow_activity.data["id"]
end
end
describe "blocking / unblocking" do
test "creates a block activity" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, activity} = ActivityPub.block(blocker, blocked)
assert activity.data["type"] == "Block"
assert activity.data["actor"] == blocker.ap_id
assert activity.data["object"] == blocked.ap_id
end
test "creates an undo activity for the last block" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, block_activity} = ActivityPub.block(blocker, blocked)
{:ok, activity} = ActivityPub.unblock(blocker, blocked)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == blocker.ap_id
embedded_object = activity.data["object"]
assert is_map(embedded_object)
assert embedded_object["type"] == "Block"
assert embedded_object["object"] == blocked.ap_id
assert embedded_object["id"] == block_activity.data["id"]
end
end
describe "deletion" do
test "it creates a delete activity and deletes the original object" do
note = insert(:note_activity)
object = Object.normalize(note)
{:ok, delete} = ActivityPub.delete(object)
assert delete.data["type"] == "Delete"
assert delete.data["actor"] == note.data["actor"]
assert delete.data["object"] == object.data["id"]
assert Activity.get_by_id(delete.id) != nil
assert Repo.get(Object, object.id).data["type"] == "Tombstone"
end
test "decrements user note count only for public activities" do
- user = insert(:user, info: %{note_count: 10})
+ user = insert(:user, note_count: 10)
{:ok, a1} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "public"
})
{:ok, a2} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "unlisted"
})
{:ok, a3} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "private"
})
{:ok, a4} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "direct"
})
{:ok, _} = Object.normalize(a1) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a2) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a3) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
user = User.get_cached_by_id(user.id)
- assert user.info.note_count == 10
+ assert user.note_count == 10
end
test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
user = insert(:user)
note = insert(:note_activity)
object = Object.normalize(note)
{:ok, object} =
object
|> Object.change(%{
data: %{
"actor" => object.data["actor"],
"id" => object.data["id"],
"to" => [user.ap_id],
"type" => "Note"
}
})
|> Object.update_and_set_cache()
{:ok, delete} = ActivityPub.delete(object)
assert user.ap_id in delete.data["to"]
end
test "decreases reply count" do
user = insert(:user)
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
ap_id = activity.data["id"]
{:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
{:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
{:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
{:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
_ = CommonAPI.delete(direct_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(private_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(public_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 1
_ = CommonAPI.delete(unlisted_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 0
end
end
describe "timeline post-processing" do
test "it filters broken threads" do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, user1} = User.follow(user1, user3)
assert User.following?(user1, user3)
{:ok, user2} = User.follow(user2, user3)
assert User.following?(user2, user3)
{:ok, user3} = User.follow(user3, user2)
assert User.following?(user3, user2)
{:ok, public_activity} = CommonAPI.post(user3, %{"status" => "hi 1"})
{:ok, private_activity_1} =
CommonAPI.post(user3, %{"status" => "hi 2", "visibility" => "private"})
{:ok, private_activity_2} =
CommonAPI.post(user2, %{
"status" => "hi 3",
"visibility" => "private",
"in_reply_to_status_id" => private_activity_1.id
})
{:ok, private_activity_3} =
CommonAPI.post(user3, %{
"status" => "hi 4",
"visibility" => "private",
"in_reply_to_status_id" => private_activity_2.id
})
activities =
ActivityPub.fetch_activities([user1.ap_id | user1.following])
|> Enum.map(fn a -> a.id end)
private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities
assert length(activities) == 3
activities =
ActivityPub.fetch_activities([user1.ap_id | user1.following], %{"user" => user1})
|> Enum.map(fn a -> a.id end)
assert [public_activity.id, private_activity_1.id] == activities
assert length(activities) == 2
end
end
describe "update" do
test "it creates an update activity with the new user data" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
user_data = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
{:ok, update} =
ActivityPub.update(%{
actor: user_data["id"],
to: [user.follower_address],
cc: [],
object: user_data
})
assert update.data["actor"] == user.ap_id
assert update.data["to"] == [user.follower_address]
assert embedded_object = update.data["object"]
assert embedded_object["id"] == user_data["id"]
assert embedded_object["type"] == user_data["type"]
end
end
test "returned pinned statuses" do
Pleroma.Config.put([:instance, :max_pinned_statuses], 3)
user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, activity_three} = CommonAPI.post(user, %{"status" => "HI!!!"})
CommonAPI.pin(activity_one.id, user)
user = refresh_record(user)
CommonAPI.pin(activity_two.id, user)
user = refresh_record(user)
CommonAPI.pin(activity_three.id, user)
user = refresh_record(user)
activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"})
assert 3 = length(activities)
end
describe "flag/1" do
setup do
reporter = insert(:user)
target_account = insert(:user)
content = "foobar"
{:ok, activity} = CommonAPI.post(target_account, %{"status" => content})
context = Utils.generate_context_id()
reporter_ap_id = reporter.ap_id
target_ap_id = target_account.ap_id
activity_ap_id = activity.data["id"]
activity_with_object = Activity.get_by_ap_id_with_object(activity_ap_id)
{:ok,
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: activity,
content: content,
activity_ap_id: activity_ap_id,
activity_with_object: activity_with_object,
reporter_ap_id: reporter_ap_id,
target_ap_id: target_ap_id
}}
end
test "it can create a Flag activity",
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: reported_activity,
content: content,
activity_ap_id: activity_ap_id,
activity_with_object: activity_with_object,
reporter_ap_id: reporter_ap_id,
target_ap_id: target_ap_id
} do
assert {:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
note_obj = %{
"type" => "Note",
"id" => activity_ap_id,
"content" => content,
"published" => activity_with_object.object.data["published"],
"actor" => AccountView.render("show.json", %{user: target_account})
}
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"content" => ^content,
"context" => ^context,
"object" => [^target_ap_id, ^note_obj]
}
} = activity
end
test_with_mock "strips status data from Flag, before federating it",
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: reported_activity,
content: content
},
Utils,
[:passthrough],
[] do
{:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
new_data =
put_in(activity.data, ["object"], [target_account.ap_id, reported_activity.data["id"]])
assert_called(Utils.maybe_federate(%{activity | data: new_data}))
end
end
test "fetch_activities/2 returns activities addressed to a list " do
user = insert(:user)
member = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, list} = Pleroma.List.follow(list, member)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
activity = Repo.preload(activity, :bookmark)
activity = %Activity{activity | thread_muted?: !!activity.thread_muted?}
assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity]
end
def data_uri do
File.read!("test/fixtures/avatar_data_uri")
end
describe "fetch_activities_bounded" do
test "fetches private posts for followed users" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "thought I looked cute might delete later :3",
"visibility" => "private"
})
[result] = ActivityPub.fetch_activities_bounded([user.follower_address], [])
assert result.id == activity.id
end
test "fetches only public posts for other users" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe", "visibility" => "public"})
{:ok, _private_activity} =
CommonAPI.post(user, %{
"status" => "why is tenshi eating a corndog so cute?",
"visibility" => "private"
})
[result] = ActivityPub.fetch_activities_bounded([], [user.follower_address])
assert result.id == activity.id
end
end
describe "fetch_follow_information_for_user" do
test "syncronizes following/followers counters" do
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/fuser2/followers",
following_address: "http://localhost:4001/users/fuser2/following"
)
{:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
assert info.follower_count == 527
assert info.following_count == 267
end
test "detects hidden followers" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_closed/followers?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
apply(HttpRequestMock, :request, [env])
end
end)
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
)
- {:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
- assert info.hide_followers == true
- assert info.hide_follows == false
+ {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
+ assert follow_info.hide_followers == true
+ assert follow_info.hide_follows == false
end
test "detects hidden follows" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_closed/following?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
apply(HttpRequestMock, :request, [env])
end
end)
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
)
- {:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
- assert info.hide_followers == false
- assert info.hide_follows == true
+ {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
+ assert follow_info.hide_followers == false
+ assert follow_info.hide_follows == true
end
end
end
diff --git a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs
index 03dc299ec..b524fdd23 100644
--- a/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs
+++ b/test/web/activity_pub/mrf/anti_link_spam_policy_test.exs
@@ -1,145 +1,145 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do
use Pleroma.DataCase
import Pleroma.Factory
import ExUnit.CaptureLog
alias Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy
@linkless_message %{
"type" => "Create",
"object" => %{
"content" => "hi world!"
}
}
@linkful_message %{
"type" => "Create",
"object" => %{
"content" => "<a href='https://example.com'>hi world!</a>"
}
}
@response_message %{
"type" => "Create",
"object" => %{
"name" => "yes",
"type" => "Answer"
}
}
describe "with new user" do
test "it allows posts without links" do
user = insert(:user)
- assert user.info.note_count == 0
+ assert user.note_count == 0
message =
@linkless_message
|> Map.put("actor", user.ap_id)
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
end
test "it disallows posts with links" do
user = insert(:user)
- assert user.info.note_count == 0
+ assert user.note_count == 0
message =
@linkful_message
|> Map.put("actor", user.ap_id)
{:reject, _} = AntiLinkSpamPolicy.filter(message)
end
end
describe "with old user" do
test "it allows posts without links" do
- user = insert(:user, info: %{note_count: 1})
+ user = insert(:user, note_count: 1)
- assert user.info.note_count == 1
+ assert user.note_count == 1
message =
@linkless_message
|> Map.put("actor", user.ap_id)
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
end
test "it allows posts with links" do
- user = insert(:user, info: %{note_count: 1})
+ user = insert(:user, note_count: 1)
- assert user.info.note_count == 1
+ assert user.note_count == 1
message =
@linkful_message
|> Map.put("actor", user.ap_id)
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
end
end
describe "with followed new user" do
test "it allows posts without links" do
- user = insert(:user, info: %{follower_count: 1})
+ user = insert(:user, follower_count: 1)
- assert user.info.follower_count == 1
+ assert user.follower_count == 1
message =
@linkless_message
|> Map.put("actor", user.ap_id)
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
end
test "it allows posts with links" do
- user = insert(:user, info: %{follower_count: 1})
+ user = insert(:user, follower_count: 1)
- assert user.info.follower_count == 1
+ assert user.follower_count == 1
message =
@linkful_message
|> Map.put("actor", user.ap_id)
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
end
end
describe "with unknown actors" do
test "it rejects posts without links" do
message =
@linkless_message
|> Map.put("actor", "http://invalid.actor")
assert capture_log(fn ->
{:reject, _} = AntiLinkSpamPolicy.filter(message)
end) =~ "[error] Could not decode user at fetch http://invalid.actor"
end
test "it rejects posts with links" do
message =
@linkful_message
|> Map.put("actor", "http://invalid.actor")
assert capture_log(fn ->
{:reject, _} = AntiLinkSpamPolicy.filter(message)
end) =~ "[error] Could not decode user at fetch http://invalid.actor"
end
end
describe "with contentless-objects" do
test "it does not reject them or error out" do
- user = insert(:user, info: %{note_count: 1})
+ user = insert(:user, note_count: 1)
message =
@response_message
|> Map.put("actor", user.ap_id)
{:ok, _message} = AntiLinkSpamPolicy.filter(message)
end
end
end
diff --git a/test/web/activity_pub/publisher_test.exs b/test/web/activity_pub/publisher_test.exs
index df03b4008..e885e5a5a 100644
--- a/test/web/activity_pub/publisher_test.exs
+++ b/test/web/activity_pub/publisher_test.exs
@@ -1,338 +1,328 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.PublisherTest do
use Pleroma.Web.ConnCase
import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
import Mock
alias Pleroma.Activity
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.CommonAPI
@as_public "https://www.w3.org/ns/activitystreams#Public"
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "determine_inbox/2" do
test "it returns sharedInbox for messages involving as:Public in to" do
user =
insert(:user, %{
- info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}}
+ source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}
})
activity = %Activity{
data: %{"to" => [@as_public], "cc" => [user.follower_address]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving as:Public in cc" do
user =
insert(:user, %{
- info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}}
+ source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}
})
activity = %Activity{
data: %{"cc" => [@as_public], "to" => [user.follower_address]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving multiple recipients in to" do
user =
insert(:user, %{
- info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}}
+ source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}
})
user_two = insert(:user)
user_three = insert(:user)
activity = %Activity{
data: %{"cc" => [], "to" => [user.ap_id, user_two.ap_id, user_three.ap_id]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving multiple recipients in cc" do
user =
insert(:user, %{
- info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}}
+ source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}
})
user_two = insert(:user)
user_three = insert(:user)
activity = %Activity{
data: %{"to" => [], "cc" => [user.ap_id, user_two.ap_id, user_three.ap_id]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving multiple recipients in total" do
user =
- insert(:user, %{
- info: %{
- source_data: %{
- "inbox" => "http://example.com/personal-inbox",
- "endpoints" => %{"sharedInbox" => "http://example.com/inbox"}
- }
+ insert(:user,
+ source_data: %{
+ "inbox" => "http://example.com/personal-inbox",
+ "endpoints" => %{"sharedInbox" => "http://example.com/inbox"}
}
- })
+ )
user_two = insert(:user)
activity = %Activity{
data: %{"to" => [user_two.ap_id], "cc" => [user.ap_id]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns inbox for messages involving single recipients in total" do
user =
- insert(:user, %{
- info: %{
- source_data: %{
- "inbox" => "http://example.com/personal-inbox",
- "endpoints" => %{"sharedInbox" => "http://example.com/inbox"}
- }
+ insert(:user,
+ source_data: %{
+ "inbox" => "http://example.com/personal-inbox",
+ "endpoints" => %{"sharedInbox" => "http://example.com/inbox"}
}
- })
+ )
activity = %Activity{
data: %{"to" => [user.ap_id], "cc" => []}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox"
end
end
describe "publish_one/1" do
test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_reachable(inbox))
end
test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} =
Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
id: 1,
unreachable_since: NaiveDateTime.utc_now()
})
assert called(Instances.set_reachable(inbox))
end
test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} =
Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
id: 1,
unreachable_since: nil
})
refute called(Instances.set_reachable(inbox))
end
test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://404.site/users/nick1/inbox"
assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_unreachable(inbox))
end
test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://connrefused.site/users/nick1/inbox"
assert capture_log(fn ->
assert {:error, _} =
Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
end) =~ "connrefused"
assert called(Instances.set_unreachable(inbox))
end
test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
refute called(Instances.set_unreachable(inbox))
end
test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`",
Instances,
[:passthrough],
[] do
actor = insert(:user)
inbox = "http://connrefused.site/users/nick1/inbox"
assert capture_log(fn ->
assert {:error, _} =
Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
id: 1,
unreachable_since: NaiveDateTime.utc_now()
})
end) =~ "connrefused"
refute called(Instances.set_unreachable(inbox))
end
end
describe "publish/2" do
test_with_mock "publishes an activity with BCC to all relevant peers.",
Pleroma.Web.Federator.Publisher,
[:passthrough],
[] do
follower =
insert(:user,
local: false,
- info: %{
- ap_enabled: true,
- source_data: %{"inbox" => "https://domain.com/users/nick1/inbox"}
- }
+ source_data: %{"inbox" => "https://domain.com/users/nick1/inbox"},
+ ap_enabled: true
)
actor = insert(:user, follower_address: follower.ap_id)
user = insert(:user)
{:ok, _follower_one} = Pleroma.User.follow(follower, actor)
actor = refresh_record(actor)
note_activity =
insert(:note_activity,
recipients: [follower.ap_id],
data_attrs: %{"bcc" => [user.ap_id]}
)
res = Publisher.publish(actor, note_activity)
assert res == :ok
assert called(
Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
inbox: "https://domain.com/users/nick1/inbox",
actor_id: actor.id,
id: note_activity.data["id"]
})
)
end
test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.",
Pleroma.Web.Federator.Publisher,
[:passthrough],
[] do
fetcher =
insert(:user,
local: false,
- info: %{
- ap_enabled: true,
- source_data: %{"inbox" => "https://domain.com/users/nick1/inbox"}
- }
+ source_data: %{"inbox" => "https://domain.com/users/nick1/inbox"},
+ ap_enabled: true
)
another_fetcher =
insert(:user,
local: false,
- info: %{
- ap_enabled: true,
- source_data: %{"inbox" => "https://domain2.com/users/nick1/inbox"}
- }
+ source_data: %{"inbox" => "https://domain2.com/users/nick1/inbox"},
+ ap_enabled: true
)
actor = insert(:user)
note_activity = insert(:note_activity, user: actor)
object = Object.normalize(note_activity)
activity_path = String.trim_leading(note_activity.data["id"], Pleroma.Web.Endpoint.url())
object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, fetcher)
|> get(object_path)
|> json_response(200)
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, another_fetcher)
|> get(activity_path)
|> json_response(200)
{:ok, delete} = CommonAPI.delete(note_activity.id, actor)
res = Publisher.publish(actor, delete)
assert res == :ok
assert called(
Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
inbox: "https://domain.com/users/nick1/inbox",
actor_id: actor.id,
id: delete.data["id"]
})
)
assert called(
Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
inbox: "https://domain2.com/users/nick1/inbox",
actor_id: actor.id,
id: delete.data["id"]
})
)
end
end
end
diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs
index 0f7556538..ac2007b2c 100644
--- a/test/web/activity_pub/relay_test.exs
+++ b/test/web/activity_pub/relay_test.exs
@@ -1,119 +1,125 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.RelayTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
+ alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
import ExUnit.CaptureLog
import Pleroma.Factory
import Mock
test "gets an actor for the relay" do
user = Relay.get_actor()
assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay"
end
+ test "relay actor is invisible" do
+ user = Relay.get_actor()
+ assert User.invisible?(user)
+ end
+
describe "follow/1" do
test "returns errors when user not found" do
assert capture_log(fn ->
- assert Relay.follow("test-ap-id") == {:error, "Could not fetch by AP id"}
- end) =~ "Could not fetch by AP id"
+ {:error, _} = Relay.follow("test-ap-id")
+ end) =~ "Could not decode user at fetch"
end
test "returns activity" do
user = insert(:user)
service_actor = Relay.get_actor()
assert {:ok, %Activity{} = activity} = Relay.follow(user.ap_id)
assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay"
assert user.ap_id in activity.recipients
assert activity.data["type"] == "Follow"
assert activity.data["actor"] == service_actor.ap_id
assert activity.data["object"] == user.ap_id
end
end
describe "unfollow/1" do
test "returns errors when user not found" do
assert capture_log(fn ->
- assert Relay.unfollow("test-ap-id") == {:error, "Could not fetch by AP id"}
- end) =~ "Could not fetch by AP id"
+ {:error, _} = Relay.unfollow("test-ap-id")
+ end) =~ "Could not decode user at fetch"
end
test "returns activity" do
user = insert(:user)
service_actor = Relay.get_actor()
ActivityPub.follow(service_actor, user)
Pleroma.User.follow(service_actor, user)
assert "#{user.ap_id}/followers" in refresh_record(service_actor).following
assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id)
assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay"
assert user.ap_id in activity.recipients
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == service_actor.ap_id
assert activity.data["to"] == [user.ap_id]
refute "#{user.ap_id}/followers" in refresh_record(service_actor).following
end
end
describe "publish/1" do
clear_config([:instance, :federating])
test "returns error when activity not `Create` type" do
activity = insert(:like_activity)
assert Relay.publish(activity) == {:error, "Not implemented"}
end
test "returns error when activity not public" do
activity = insert(:direct_note_activity)
assert Relay.publish(activity) == {:error, false}
end
test "returns error when object is unknown" do
activity =
insert(:note_activity,
data: %{
"type" => "Create",
"object" => "http://mastodon.example.org/eee/99541947525187367"
}
)
assert capture_log(fn ->
assert Relay.publish(activity) == {:error, nil}
end) =~ "[error] error: nil"
end
test_with_mock "returns announce activity and publish to federate",
Pleroma.Web.Federator,
[:passthrough],
[] do
Pleroma.Config.put([:instance, :federating], true)
service_actor = Relay.get_actor()
note = insert(:note_activity)
assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note)
assert activity.data["type"] == "Announce"
assert activity.data["actor"] == service_actor.ap_id
assert activity.data["object"] == obj.data["id"]
assert called(Pleroma.Web.Federator.publish(activity))
end
test_with_mock "returns announce activity and not publish to federate",
Pleroma.Web.Federator,
[:passthrough],
[] do
Pleroma.Config.put([:instance, :federating], false)
service_actor = Relay.get_actor()
note = insert(:note_activity)
assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note)
assert activity.data["type"] == "Announce"
assert activity.data["actor"] == service_actor.ap_id
assert activity.data["object"] == obj.data["id"]
refute called(Pleroma.Web.Federator.publish(activity))
end
end
end
diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
index 99ab573c5..75cfbea2e 100644
--- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs
+++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
@@ -1,162 +1,162 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
import Pleroma.Factory
import Ecto.Query
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "handle_incoming" do
test "it works for osada follow request" do
user = insert(:user)
data =
File.read!("test/fixtures/osada-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "https://apfed.club/channel/indio"
assert data["type"] == "Follow"
assert data["id"] == "https://apfed.club/follow/9"
activity = Repo.get(Activity, activity.id)
assert activity.data["state"] == "accept"
assert User.following?(User.get_cached_by_ap_id(data["actor"]), user)
end
test "it works for incoming follow requests" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Follow"
assert data["id"] == "http://mastodon.example.org/users/admin#follows/2"
activity = Repo.get(Activity, activity.id)
assert activity.data["state"] == "accept"
assert User.following?(User.get_cached_by_ap_id(data["actor"]), user)
end
test "with locked accounts, it does not create a follow or an accept" do
- user = insert(:user, info: %{locked: true})
+ user = insert(:user, locked: true)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["state"] == "pending"
refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
accepts =
from(
a in Activity,
where: fragment("?->>'type' = ?", a.data, "Accept")
)
|> Repo.all()
assert length(accepts) == 0
end
test "it works for follow requests when you are already followed, creating a new accept activity" do
# This is important because the remote might have the wrong idea about the
# current follow status. This can lead to instance A thinking that x@A is
# followed by y@B, but B thinks they are not. In this case, the follow can
# never go through again because it will never get an Accept.
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
accepts =
from(
a in Activity,
where: fragment("?->>'type' = ?", a.data, "Accept")
)
|> Repo.all()
assert length(accepts) == 1
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("id", String.replace(data["id"], "2", "3"))
|> Map.put("object", user.ap_id)
{:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
accepts =
from(
a in Activity,
where: fragment("?->>'type' = ?", a.data, "Accept")
)
|> Repo.all()
assert length(accepts) == 2
end
test "it rejects incoming follow requests from blocked users when deny_follow_blocked is enabled" do
Pleroma.Config.put([:user, :deny_follow_blocked], true)
user = insert(:user)
{:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin")
{:ok, user} = User.block(user, target)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data)
%Activity{} = activity = Activity.get_by_ap_id(id)
assert activity.data["state"] == "reject"
end
test "it works for incoming follow requests from hubzilla" do
user = insert(:user)
data =
File.read!("test/fixtures/hubzilla-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
|> Utils.normalize_params()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "https://hubzilla.example.org/channel/kaniini"
assert data["type"] == "Follow"
assert data["id"] == "https://hubzilla.example.org/channel/kaniini#follows/2"
assert User.following?(User.get_cached_by_ap_id(data["actor"]), user)
end
end
end
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 82389b344..1069ade02 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1,1851 +1,1851 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
- alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.Websub.WebsubClientSubscription
import Mock
import Pleroma.Factory
import ExUnit.CaptureLog
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
clear_config([:instance, :max_remote_account_fields])
describe "handle_incoming" do
test "it ignores an incoming notice if we already have it" do
activity = insert(:note_activity)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("object", Object.normalize(activity).data)
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
assert activity == returned_activity
end
test "it fetches replied-to activities if we don't have them" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
data = Map.put(data, "object", object)
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
returned_object = Object.normalize(returned_activity, false)
assert activity =
Activity.get_create_by_object_ap_id(
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
)
assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
end
test "it does not fetch replied-to activities beyond max_replies_depth" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
data = Map.put(data, "object", object)
with_mock Pleroma.Web.Federator,
allowed_incoming_reply_depth?: fn _ -> false end do
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
returned_object = Object.normalize(returned_activity, false)
refute Activity.get_create_by_object_ap_id(
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
)
assert returned_object.data["inReplyToAtomUri"] ==
"https://shitposter.club/notice/2827873"
end
end
test "it does not crash if the object in inReplyTo can't be fetched" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("inReplyTo", "https://404.site/whatever")
data =
data
|> Map.put("object", object)
assert capture_log(fn ->
{:ok, _returned_activity} = Transmogrifier.handle_incoming(data)
end) =~ "[error] Couldn't fetch \"https://404.site/whatever\", error: nil"
end
test "it works for incoming notices" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99512778738411822/activity"
assert data["context"] ==
"tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert data["cc"] == [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
assert data["actor"] == "http://mastodon.example.org/users/admin"
object_data = Object.normalize(data["object"]).data
assert object_data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99512778738411822"
assert object_data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert object_data["cc"] == [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
assert object_data["actor"] == "http://mastodon.example.org/users/admin"
assert object_data["attributedTo"] == "http://mastodon.example.org/users/admin"
assert object_data["context"] ==
"tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert object_data["sensitive"] == true
user = User.get_cached_by_ap_id(object_data["actor"])
- assert user.info.note_count == 1
+ assert user.note_count == 1
end
test "it works for incoming notices with hashtags" do
data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert Enum.at(object.data["tag"], 2) == "moo"
end
test "it works for incoming questions" do
data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!()
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity)
assert Enum.all?(object.data["oneOf"], fn choice ->
choice["name"] in [
"Dunno",
"Everyone knows that!",
"25 char limit is dumb",
"I can't even fit a funny"
]
end)
end
test "it works for incoming listens" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Listen",
"id" => "http://mastodon.example.org/users/admin/listens/1234/activity",
"actor" => "http://mastodon.example.org/users/admin",
"object" => %{
"type" => "Audio",
"id" => "http://mastodon.example.org/users/admin/listens/1234",
"attributedTo" => "http://mastodon.example.org/users/admin",
"title" => "lain radio episode 1",
"artist" => "lain",
"album" => "lain radio",
"length" => 180_000
}
}
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity)
assert object.data["title"] == "lain radio episode 1"
assert object.data["artist"] == "lain"
assert object.data["album"] == "lain radio"
assert object.data["length"] == 180_000
end
test "it rewrites Note votes to Answers and increments vote counters on question activities" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "suya...",
"poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10}
})
object = Object.normalize(activity)
data =
File.read!("test/fixtures/mastodon-vote.json")
|> Poison.decode!()
|> Kernel.put_in(["to"], user.ap_id)
|> Kernel.put_in(["object", "inReplyTo"], object.data["id"])
|> Kernel.put_in(["object", "to"], user.ap_id)
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
answer_object = Object.normalize(activity)
assert answer_object.data["type"] == "Answer"
object = Object.get_by_ap_id(object.data["id"])
assert Enum.any?(
object.data["oneOf"],
fn
%{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true
_ -> false
end
)
end
test "it works for incoming notices with contentMap" do
data =
File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert object.data["content"] ==
"<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
end
test "it works for incoming notices with to/cc not being an array (kroeg)" do
data = File.read!("test/fixtures/kroeg-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert object.data["content"] ==
"<p>henlo from my Psion netBook</p><p>message sent from my Psion netBook</p>"
end
test "it works for incoming announces with actor being inlined (kroeg)" do
data = File.read!("test/fixtures/kroeg-announce-with-inline-actor.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "https://puckipedia.com/"
end
test "it works for incoming notices with tag not being an array (kroeg)" do
data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert object.data["emoji"] == %{
"icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png"
}
data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert "test" in object.data["tag"]
end
test "it works for incoming notices with url not being a string (prismo)" do
data = File.read!("test/fixtures/prismo-url-map.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"])
assert object.data["url"] == "https://prismo.news/posts/83"
end
test "it cleans up incoming notices which are not really DMs" do
user = insert(:user)
other_user = insert(:user)
to = [user.ap_id, other_user.ap_id]
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("to", to)
|> Map.put("cc", [])
object =
data["object"]
|> Map.put("to", to)
|> Map.put("cc", [])
data = Map.put(data, "object", object)
{:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
assert data["to"] == []
assert data["cc"] == to
object_data = Object.normalize(activity).data
assert object_data["to"] == []
assert object_data["cc"] == to
end
test "it works for incoming likes" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Like"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
assert data["object"] == activity.data["object"]
end
test "it returns an error for incoming unlikes wihout a like activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
assert Transmogrifier.handle_incoming(data) == :error
end
test "it works for incoming unlikes with an existing like activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
like_data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", like_data)
|> Map.put("actor", like_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Undo"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
end
test "it works for incoming unlikes with an existing like activity and a compact object" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
like_data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", like_data["id"])
|> Map.put("actor", like_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Undo"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
end
test "it works for incoming announces" do
data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce"
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["object"] ==
"http://mastodon.example.org/users/admin/statuses/99541947525187367"
assert Activity.get_create_by_object_ap_id(data["object"])
end
test "it works for incoming announces with an existing activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce"
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["object"] == activity.data["object"]
assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id
end
test "it works for incoming announces with an inlined activity" do
data =
File.read!("test/fixtures/mastodon-announce-private.json")
|> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce"
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
object = Object.normalize(data["object"])
assert object.data["id"] == "http://mastodon.example.org/@admin/99541947525187368"
assert object.data["content"] == "this is a private toot"
end
test "it rejects incoming announces with an inlined activity from another origin" do
data =
File.read!("test/fixtures/bogus-mastodon-announce.json")
|> Poison.decode!()
assert :error = Transmogrifier.handle_incoming(data)
end
test "it does not clobber the addressing on announce activities" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", Object.normalize(activity).data["id"])
|> Map.put("to", ["http://mastodon.example.org/users/admin/followers"])
|> Map.put("cc", [])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["to"] == ["http://mastodon.example.org/users/admin/followers"]
end
test "it ensures that as:Public activities make it to their followers collection" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("actor", user.ap_id)
|> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
|> Map.put("cc", [])
object =
data["object"]
|> Map.put("attributedTo", user.ap_id)
|> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
|> Map.put("cc", [])
|> Map.put("id", user.ap_id <> "/activities/12345678")
data = Map.put(data, "object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["cc"] == [User.ap_followers(user)]
end
test "it ensures that address fields become lists" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("actor", user.ap_id)
|> Map.put("to", nil)
|> Map.put("cc", nil)
object =
data["object"]
|> Map.put("attributedTo", user.ap_id)
|> Map.put("to", nil)
|> Map.put("cc", nil)
|> Map.put("id", user.ap_id <> "/activities/12345678")
data = Map.put(data, "object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert !is_nil(data["to"])
assert !is_nil(data["cc"])
end
test "it strips internal likes" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
likes = %{
"first" =>
"http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1",
"id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes",
"totalItems" => 3,
"type" => "OrderedCollection"
}
object = Map.put(data["object"], "likes", likes)
data = Map.put(data, "object", object)
{:ok, %Activity{object: object}} = Transmogrifier.handle_incoming(data)
refute Map.has_key?(object.data, "likes")
end
test "it works for incoming update activities" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
object =
update_data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("id", data["actor"])
update_data =
update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
assert data["id"] == update_data["id"]
user = User.get_cached_by_ap_id(data["actor"])
assert user.name == "gargle"
assert user.avatar["url"] == [
%{
"href" =>
"https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
}
]
- assert user.info.banner["url"] == [
+ assert user.banner["url"] == [
%{
"href" =>
"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
]
assert user.bio == "<p>Some bio</p>"
end
test "it works with custom profile fields" do
{:ok, activity} =
"test/fixtures/mastodon-post-activity.json"
|> File.read!()
|> Poison.decode!()
|> Transmogrifier.handle_incoming()
user = User.get_cached_by_ap_id(activity.actor)
- assert User.Info.fields(user.info) == [
+ assert User.fields(user) == [
%{"name" => "foo", "value" => "bar"},
%{"name" => "foo1", "value" => "bar1"}
]
update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
object =
update_data["object"]
|> Map.put("actor", user.ap_id)
|> Map.put("id", user.ap_id)
update_data =
update_data
|> Map.put("actor", user.ap_id)
|> Map.put("object", object)
{:ok, _update_activity} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(user.ap_id)
- assert User.Info.fields(user.info) == [
+ assert User.fields(user) == [
%{"name" => "foo", "value" => "updated"},
%{"name" => "foo1", "value" => "updated"}
]
Pleroma.Config.put([:instance, :max_remote_account_fields], 2)
update_data =
put_in(update_data, ["object", "attachment"], [
%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"},
%{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"},
%{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"}
])
{:ok, _} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(user.ap_id)
- assert User.Info.fields(user.info) == [
+ assert User.fields(user) == [
%{"name" => "foo", "value" => "updated"},
%{"name" => "foo1", "value" => "updated"}
]
update_data = put_in(update_data, ["object", "attachment"], [])
{:ok, _} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(user.ap_id)
- assert User.Info.fields(user.info) == []
+ assert User.fields(user) == []
end
test "it works for incoming update activities which lock the account" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
object =
update_data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("id", data["actor"])
|> Map.put("manuallyApprovesFollowers", true)
update_data =
update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(data["actor"])
- assert user.info.locked == true
+ assert user.locked == true
end
test "it works for incoming deletes" do
activity = insert(:note_activity)
deleting_user = insert(:user)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("id", activity.data["object"])
data =
data
|> Map.put("object", object)
|> Map.put("actor", deleting_user.ap_id)
{:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} =
Transmogrifier.handle_incoming(data)
assert id == data["id"]
refute Activity.get_by_id(activity.id)
assert actor == deleting_user.ap_id
end
test "it fails for incoming deletes with spoofed origin" do
activity = insert(:note_activity)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("id", activity.data["object"])
data =
data
|> Map.put("object", object)
assert capture_log(fn ->
:error = Transmogrifier.handle_incoming(data)
end) =~
- "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, {:error, :nxdomain}}"
+ "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, :nxdomain}"
assert Activity.get_by_id(activity.id)
end
test "it works for incoming user deletes" do
%{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Poison.decode!()
{:ok, _} = Transmogrifier.handle_incoming(data)
ObanHelpers.perform_all()
refute User.get_cached_by_ap_id(ap_id)
end
test "it fails for incoming user deletes with spoofed origin" do
%{ap_id: ap_id} = insert(:user)
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Poison.decode!()
|> Map.put("actor", ap_id)
assert :error == Transmogrifier.handle_incoming(data)
assert User.get_cached_by_ap_id(ap_id)
end
test "it works for incoming unannounces with an existing notice" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
announce_data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: announce_data, local: false}} =
Transmogrifier.handle_incoming(announce_data)
data =
File.read!("test/fixtures/mastodon-undo-announce.json")
|> Poison.decode!()
|> Map.put("object", announce_data)
|> Map.put("actor", announce_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert object_data = data["object"]
assert object_data["type"] == "Announce"
assert object_data["object"] == activity.data["object"]
assert object_data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
end
test "it works for incomming unfollows with an existing follow" do
user = insert(:user)
follow_data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
data =
File.read!("test/fixtures/mastodon-unfollow-activity.json")
|> Poison.decode!()
|> Map.put("object", follow_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Follow"
assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
end
test "it works for incoming blocks" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Block"
assert data["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
blocker = User.get_cached_by_ap_id(data["actor"])
assert User.blocks?(blocker, user)
end
test "incoming blocks successfully tear down any follow relationship" do
blocker = insert(:user)
blocked = insert(:user)
data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", blocked.ap_id)
|> Map.put("actor", blocker.ap_id)
{:ok, blocker} = User.follow(blocker, blocked)
{:ok, blocked} = User.follow(blocked, blocker)
assert User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Block"
assert data["object"] == blocked.ap_id
assert data["actor"] == blocker.ap_id
blocker = User.get_cached_by_ap_id(data["actor"])
blocked = User.get_cached_by_ap_id(data["object"])
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "it works for incoming unblocks with an existing block" do
user = insert(:user)
block_data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
data =
File.read!("test/fixtures/mastodon-unblock-activity.json")
|> Poison.decode!()
|> Map.put("object", block_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Block"
assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
blocker = User.get_cached_by_ap_id(data["actor"])
refute User.blocks?(blocker, user)
end
test "it works for incoming accepts which were pre-accepted" do
follower = insert(:user)
followed = insert(:user)
{:ok, follower} = User.follow(follower, followed)
assert User.following?(follower, followed) == true
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
object =
accept_data["object"]
|> Map.put("actor", follower.ap_id)
|> Map.put("id", follow_activity.data["id"])
accept_data = Map.put(accept_data, "object", object)
{:ok, activity} = Transmogrifier.handle_incoming(accept_data)
refute activity.local
assert activity.data["object"] == follow_activity.data["id"]
assert activity.data["id"] == accept_data["id"]
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == true
end
test "it works for incoming accepts which were orphaned" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
accept_data =
Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
{:ok, activity} = Transmogrifier.handle_incoming(accept_data)
assert activity.data["object"] == follow_activity.data["id"]
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == true
end
test "it works for incoming accepts which are referenced by IRI only" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
|> Map.put("object", follow_activity.data["id"])
{:ok, activity} = Transmogrifier.handle_incoming(accept_data)
assert activity.data["object"] == follow_activity.data["id"]
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == true
end
test "it fails for incoming accepts which cannot be correlated" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
accept_data =
Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
:error = Transmogrifier.handle_incoming(accept_data)
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, followed) == true
end
test "it fails for incoming rejects which cannot be correlated" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
accept_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
accept_data =
Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
:error = Transmogrifier.handle_incoming(accept_data)
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, followed) == true
end
test "it works for incoming rejects which are orphaned" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
{:ok, follower} = User.follow(follower, followed)
{:ok, _follow_activity} = ActivityPub.follow(follower, followed)
assert User.following?(follower, followed) == true
reject_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
reject_data =
Map.put(reject_data, "object", Map.put(reject_data["object"], "actor", follower.ap_id))
{:ok, activity} = Transmogrifier.handle_incoming(reject_data)
refute activity.local
assert activity.data["id"] == reject_data["id"]
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == false
end
test "it works for incoming rejects which are referenced by IRI only" do
follower = insert(:user)
- followed = insert(:user, %{info: %User.Info{locked: true}})
+ followed = insert(:user, locked: true)
{:ok, follower} = User.follow(follower, followed)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
assert User.following?(follower, followed) == true
reject_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
|> Poison.decode!()
|> Map.put("actor", followed.ap_id)
|> Map.put("object", follow_activity.data["id"])
{:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data)
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == false
end
test "it rejects activities without a valid ID" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
|> Map.put("id", "")
:error = Transmogrifier.handle_incoming(data)
end
test "it remaps video URLs as attachments if necessary" do
{:ok, object} =
Fetcher.fetch_object_from_id(
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
)
attachment = %{
"type" => "Link",
"mediaType" => "video/mp4",
"href" =>
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
"mimeType" => "video/mp4",
"size" => 5_015_880,
"url" => [
%{
"href" =>
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
],
"width" => 480
}
assert object.data["url"] ==
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
assert object.data["attachment"] == [attachment]
end
test "it accepts Flag activities" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
object = Object.normalize(activity)
note_obj = %{
"type" => "Note",
"id" => activity.data["id"],
"content" => "test post",
"published" => object.data["published"],
"actor" => AccountView.render("show.json", %{user: user})
}
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"cc" => [user.ap_id],
"object" => [user.ap_id, activity],
"type" => "Flag",
"content" => "blocked AND reported!!!",
"actor" => other_user.ap_id
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
assert activity.data["object"] == [user.ap_id, note_obj]
assert activity.data["content"] == "blocked AND reported!!!"
assert activity.data["actor"] == other_user.ap_id
assert activity.data["cc"] == [user.ap_id]
end
+
+ test "it correctly processes messages with non-array to field" do
+ user = insert(:user)
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "to" => "https://www.w3.org/ns/activitystreams#Public",
+ "type" => "Create",
+ "object" => %{
+ "content" => "blah blah blah",
+ "type" => "Note",
+ "attributedTo" => user.ap_id,
+ "inReplyTo" => nil
+ },
+ "actor" => user.ap_id
+ }
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"]
+ end
+
+ test "it correctly processes messages with non-array cc field" do
+ user = insert(:user)
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "to" => user.follower_address,
+ "cc" => "https://www.w3.org/ns/activitystreams#Public",
+ "type" => "Create",
+ "object" => %{
+ "content" => "blah blah blah",
+ "type" => "Note",
+ "attributedTo" => user.ap_id,
+ "inReplyTo" => nil
+ },
+ "actor" => user.ap_id
+ }
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"]
+ assert [user.follower_address] == activity.data["to"]
+ end
end
describe "prepare outgoing" do
test "it inlines private announced objects" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey", "visibility" => "private"})
{:ok, announce_activity, _} = CommonAPI.repeat(activity.id, user)
{:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data)
assert modified["object"]["content"] == "hey"
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
test "it turns mentions into tags" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "hey, @#{other_user.nickname}, how are ya? #2hu"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
object = modified["object"]
expected_mention = %{
"href" => other_user.ap_id,
"name" => "@#{other_user.nickname}",
"type" => "Mention"
}
expected_tag = %{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
"type" => "Hashtag",
"name" => "#2hu"
}
assert Enum.member?(object["tag"], expected_tag)
assert Enum.member?(object["tag"], expected_mention)
end
test "it adds the sensitive property" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#nsfw hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["sensitive"]
end
test "it adds the json-ld context and the conversation property" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["@context"] ==
Pleroma.Web.ActivityPub.Utils.make_json_ld_header()["@context"]
assert modified["object"]["conversation"] == modified["context"]
end
test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
- test "it translates ostatus IDs to external URLs" do
- incoming = File.read!("test/fixtures/incoming_note_activity.xml")
- {:ok, [referent_activity]} = OStatus.handle_incoming(incoming)
-
- user = insert(:user)
-
- {:ok, activity, _} = CommonAPI.favorite(referent_activity.id, user)
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
-
- assert modified["object"] == "http://gs.example.org:4040/index.php/notice/29"
- end
-
- test "it translates ostatus reply_to IDs to external URLs" do
- incoming = File.read!("test/fixtures/incoming_note_activity.xml")
- {:ok, [referred_activity]} = OStatus.handle_incoming(incoming)
-
- user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{"status" => "HI!", "in_reply_to_status_id" => referred_activity.id})
-
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
-
- assert modified["object"]["inReplyTo"] == "http://gs.example.org:4040/index.php/notice/29"
- end
-
test "it strips internal hashtag data" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu"})
expected_tag = %{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
"type" => "Hashtag",
"name" => "#2hu"
}
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["tag"] == [expected_tag]
end
test "it strips internal fields" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu :firefox:"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert length(modified["object"]["tag"]) == 2
assert is_nil(modified["object"]["emoji"])
assert is_nil(modified["object"]["like_count"])
assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["context_id"])
end
test "it strips internal fields of article" do
activity = insert(:article_activity)
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert length(modified["object"]["tag"]) == 2
assert is_nil(modified["object"]["emoji"])
assert is_nil(modified["object"]["like_count"])
assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["context_id"])
assert is_nil(modified["object"]["likes"])
end
test "the directMessage flag is present" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "2hu :moominmamma:"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["directMessage"] == false
{:ok, activity} =
CommonAPI.post(user, %{"status" => "@#{other_user.nickname} :moominmamma:"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["directMessage"] == false
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "@#{other_user.nickname} :moominmamma:",
"visibility" => "direct"
})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["directMessage"] == true
end
test "it strips BCC field" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert is_nil(modified["bcc"])
end
test "it can handle Listen activities" do
listen_activity = insert(:listen)
{:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data)
assert modified["type"] == "Listen"
user = insert(:user)
{:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"})
{:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data)
end
end
describe "user upgrade" do
test "it upgrades a user to activitypub" do
user =
insert(:user, %{
nickname: "rye@niu.moe",
local: false,
ap_id: "https://niu.moe/users/rye",
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
})
user_two = insert(:user, %{following: [user.follower_address]})
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})
assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
user = User.get_cached_by_id(user.id)
- assert user.info.note_count == 1
+ assert user.note_count == 1
{:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
ObanHelpers.perform_all()
- assert user.info.ap_enabled
- assert user.info.note_count == 1
+ assert user.ap_enabled
+ assert user.note_count == 1
assert user.follower_address == "https://niu.moe/users/rye/followers"
assert user.following_address == "https://niu.moe/users/rye/following"
user = User.get_cached_by_id(user.id)
- assert user.info.note_count == 1
+ assert user.note_count == 1
activity = Activity.get_by_id(activity.id)
assert user.follower_address in activity.recipients
assert %{
"url" => [
%{
"href" =>
"https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
}
]
} = user.avatar
assert %{
"url" => [
%{
"href" =>
"https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
]
- } = user.info.banner
+ } = user.banner
refute "..." in activity.recipients
unrelated_activity = Activity.get_by_id(unrelated_activity.id)
refute user.follower_address in unrelated_activity.recipients
user_two = User.get_cached_by_id(user_two.id)
assert user.follower_address in user_two.following
refute "..." in user_two.following
end
end
- describe "maybe_retire_websub" do
- test "it deletes all websub client subscripitions with the user as topic" do
- subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/rye.atom"}
- {:ok, ws} = Repo.insert(subscription)
-
- subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/pasty.atom"}
- {:ok, ws2} = Repo.insert(subscription)
-
- Transmogrifier.maybe_retire_websub("https://niu.moe/users/rye")
-
- refute Repo.get(WebsubClientSubscription, ws.id)
- assert Repo.get(WebsubClientSubscription, ws2.id)
- end
- end
-
describe "actor rewriting" do
test "it fixes the actor URL property to be a proper URI" do
data = %{
"url" => %{"href" => "http://example.com"}
}
rewritten = Transmogrifier.maybe_fix_user_object(data)
assert rewritten["url"] == "http://example.com"
end
end
describe "actor origin containment" do
test "it rejects activities which reference objects with bogus origins" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity.json",
"type" => "Announce"
}
:error = Transmogrifier.handle_incoming(data)
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity2.json",
"type" => "Announce"
}
:error = Transmogrifier.handle_incoming(data)
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity3.json",
"type" => "Announce"
}
:error = Transmogrifier.handle_incoming(data)
end
end
describe "reserialization" do
test "successfully reserializes a message with inReplyTo == nil" do
user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Create",
"object" => %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Note",
"content" => "Hi",
"inReplyTo" => nil,
"attributedTo" => user.ap_id
},
"actor" => user.ap_id
}
{:ok, activity} = Transmogrifier.handle_incoming(message)
{:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
end
test "successfully reserializes a message with AS2 objects in IR" do
user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Create",
"object" => %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Note",
"content" => "Hi",
"inReplyTo" => nil,
"attributedTo" => user.ap_id,
"tag" => [
%{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"},
%{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"}
]
},
"actor" => user.ap_id
}
{:ok, activity} = Transmogrifier.handle_incoming(message)
{:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
end
end
test "Rewrites Answers to Notes" do
user = insert(:user)
{:ok, poll_activity} =
CommonAPI.post(user, %{
"status" => "suya...",
"poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10}
})
poll_object = Object.normalize(poll_activity)
# TODO: Replace with CommonAPI vote creation when implemented
data =
File.read!("test/fixtures/mastodon-vote.json")
|> Poison.decode!()
|> Kernel.put_in(["to"], user.ap_id)
|> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"])
|> Kernel.put_in(["object", "to"], user.ap_id)
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
assert data["object"]["type"] == "Note"
end
describe "fix_explicit_addressing" do
setup do
user = insert(:user)
[user: user]
end
test "moves non-explicitly mentioned actors to cc", %{user: user} do
explicitly_mentioned_actors = [
"https://pleroma.gold/users/user1",
"https://pleroma.gold/user2"
]
object = %{
"actor" => user.ap_id,
"to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"],
"cc" => [],
"tag" =>
Enum.map(explicitly_mentioned_actors, fn href ->
%{"type" => "Mention", "href" => href}
end)
}
fixed_object = Transmogrifier.fix_explicit_addressing(object)
assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"]))
refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"]
assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"]
end
test "does not move actor's follower collection to cc", %{user: user} do
object = %{
"actor" => user.ap_id,
"to" => [user.follower_address],
"cc" => []
}
fixed_object = Transmogrifier.fix_explicit_addressing(object)
assert user.follower_address in fixed_object["to"]
refute user.follower_address in fixed_object["cc"]
end
test "removes recipient's follower collection from cc", %{user: user} do
recipient = insert(:user)
object = %{
"actor" => user.ap_id,
"to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"],
"cc" => [user.follower_address, recipient.follower_address]
}
fixed_object = Transmogrifier.fix_explicit_addressing(object)
assert user.follower_address in fixed_object["cc"]
refute recipient.follower_address in fixed_object["cc"]
refute recipient.follower_address in fixed_object["to"]
end
end
describe "fix_summary/1" do
test "returns fixed object" do
assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
end
end
describe "fix_in_reply_to/2" do
clear_config([:instance, :federation_incoming_replies_max_depth])
setup do
data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
[data: data]
end
test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do
assert Transmogrifier.fix_in_reply_to(data) == data
end
test "returns object with inReplyToAtomUri when denied incoming reply", %{data: data} do
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
object_with_reply =
Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873")
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
object_with_reply =
Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"})
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"}
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
object_with_reply =
Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
object_with_reply = Map.put(data["object"], "inReplyTo", [])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == []
assert modified_object["inReplyToAtomUri"] == ""
end
test "returns modified object when allowed incoming reply", %{data: data} do
object_with_reply =
Map.put(
data["object"],
"inReplyTo",
"https://shitposter.club/notice/2827873"
)
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5)
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] ==
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
assert modified_object["conversation"] ==
"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
assert modified_object["context"] ==
"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
end
end
describe "fix_url/1" do
test "fixes data for object when url is map" do
object = %{
"url" => %{
"type" => "Link",
"mimeType" => "video/mp4",
"href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
}
}
assert Transmogrifier.fix_url(object) == %{
"url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
}
end
test "fixes data for video object" do
object = %{
"type" => "Video",
"url" => [
%{
"type" => "Link",
"mimeType" => "video/mp4",
"href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
},
%{
"type" => "Link",
"mimeType" => "video/mp4",
"href" => "https://peertube46fb-ad81-2d4c2d1630e3-240.mp4"
},
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d1630e3"
},
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d16377-42"
}
]
}
assert Transmogrifier.fix_url(object) == %{
"attachment" => [
%{
"href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4",
"mimeType" => "video/mp4",
"type" => "Link"
}
],
"type" => "Video",
"url" => "https://peertube.-2d4c2d1630e3"
}
end
test "fixes url for not Video object" do
object = %{
"type" => "Text",
"url" => [
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d1630e3"
},
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d16377-42"
}
]
}
assert Transmogrifier.fix_url(object) == %{
"type" => "Text",
"url" => "https://peertube.-2d4c2d1630e3"
}
assert Transmogrifier.fix_url(%{"type" => "Text", "url" => []}) == %{
"type" => "Text",
"url" => ""
}
end
test "retunrs not modified object" do
assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
end
end
describe "get_obj_helper/2" do
test "returns nil when cannot normalize object" do
refute Transmogrifier.get_obj_helper("test-obj-id")
end
test "returns {:ok, %Object{}} for success case" do
assert {:ok, %Object{}} =
Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")
end
end
describe "fix_attachments/1" do
test "returns not modified object" do
data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
assert Transmogrifier.fix_attachments(data) == data
end
test "returns modified object when attachment is map" do
assert Transmogrifier.fix_attachments(%{
"attachment" => %{
"mediaType" => "video/mp4",
"url" => "https://peertube.moe/stat-480.mp4"
}
}) == %{
"attachment" => [
%{
"mediaType" => "video/mp4",
"url" => [
%{
"href" => "https://peertube.moe/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
]
}
]
}
end
test "returns modified object when attachment is list" do
assert Transmogrifier.fix_attachments(%{
"attachment" => [
%{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"},
%{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"}
]
}) == %{
"attachment" => [
%{
"mediaType" => "video/mp4",
"url" => [
%{
"href" => "https://pe.er/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
]
},
%{
"href" => "https://pe.er/stat-480.mp4",
"mediaType" => "video/mp4",
"mimeType" => "video/mp4",
"url" => [
%{
"href" => "https://pe.er/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
]
}
]
}
end
end
describe "fix_emoji/1" do
test "returns not modified object when object not contains tags" do
data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
assert Transmogrifier.fix_emoji(data) == data
end
test "returns object with emoji when object contains list tags" do
assert Transmogrifier.fix_emoji(%{
"tag" => [
%{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
%{"type" => "Hashtag"}
]
}) == %{
"emoji" => %{"bib" => "/test"},
"tag" => [
%{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
%{"type" => "Hashtag"}
]
}
end
test "returns object with emoji when object contains map tag" do
assert Transmogrifier.fix_emoji(%{
"tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
}) == %{
"emoji" => %{"bib" => "/test"},
"tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
}
end
end
end
diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs
index 4475d06d2..586eb1d2f 100644
--- a/test/web/activity_pub/utils_test.exs
+++ b/test/web/activity_pub/utils_test.exs
@@ -1,639 +1,639 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UtilsTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
require Pleroma.Constants
describe "fetch the latest Follow" do
test "fetches the latest Follow activity" do
%Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
follower = User.get_cached_by_ap_id(activity.data["actor"])
followed = User.get_cached_by_ap_id(activity.data["object"])
assert activity == Utils.fetch_latest_follow(follower, followed)
end
end
describe "fetch the latest Block" do
test "fetches the latest Block activity" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, activity} = ActivityPub.block(blocker, blocked)
assert activity == Utils.fetch_latest_block(blocker, blocked)
end
end
describe "determine_explicit_mentions()" do
test "works with an object that has mentions" do
object = %{
"tag" => [
%{
"type" => "Mention",
"href" => "https://example.com/~alyssa",
"name" => "Alyssa P. Hacker"
}
]
}
assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"]
end
test "works with an object that does not have mentions" do
object = %{
"tag" => [
%{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"}
]
}
assert Utils.determine_explicit_mentions(object) == []
end
test "works with an object that has mentions and other tags" do
object = %{
"tag" => [
%{
"type" => "Mention",
"href" => "https://example.com/~alyssa",
"name" => "Alyssa P. Hacker"
},
%{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"}
]
}
assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"]
end
test "works with an object that has no tags" do
object = %{}
assert Utils.determine_explicit_mentions(object) == []
end
test "works with an object that has only IR tags" do
object = %{"tag" => ["2hu"]}
assert Utils.determine_explicit_mentions(object) == []
end
test "works with an object has tags as map" do
object = %{
"tag" => %{
"type" => "Mention",
"href" => "https://example.com/~alyssa",
"name" => "Alyssa P. Hacker"
}
}
assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"]
end
end
describe "make_unlike_data/3" do
test "returns data for unlike activity" do
user = insert(:user)
like_activity = insert(:like_activity, data_attrs: %{"context" => "test context"})
object = Object.normalize(like_activity.data["object"])
assert Utils.make_unlike_data(user, like_activity, nil) == %{
"type" => "Undo",
"actor" => user.ap_id,
"object" => like_activity.data,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => like_activity.data["context"]
}
assert Utils.make_unlike_data(user, like_activity, "9mJEZK0tky1w2xD2vY") == %{
"type" => "Undo",
"actor" => user.ap_id,
"object" => like_activity.data,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => like_activity.data["context"],
"id" => "9mJEZK0tky1w2xD2vY"
}
end
end
describe "make_like_data" do
setup do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
[user: user, other_user: other_user, third_user: third_user]
end
test "addresses actor's follower address if the activity is public", %{
user: user,
other_user: other_user,
third_user: third_user
} do
expected_to = Enum.sort([user.ap_id, other_user.follower_address])
expected_cc = Enum.sort(["https://www.w3.org/ns/activitystreams#Public", third_user.ap_id])
{:ok, activity} =
CommonAPI.post(user, %{
"status" =>
"hey @#{other_user.nickname}, @#{third_user.nickname} how about beering together this weekend?"
})
%{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil)
assert Enum.sort(to) == expected_to
assert Enum.sort(cc) == expected_cc
end
test "does not adress actor's follower address if the activity is not public", %{
user: user,
other_user: other_user,
third_user: third_user
} do
expected_to = Enum.sort([user.ap_id])
expected_cc = [third_user.ap_id]
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "@#{other_user.nickname} @#{third_user.nickname} bought a new swimsuit!",
"visibility" => "private"
})
%{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil)
assert Enum.sort(to) == expected_to
assert Enum.sort(cc) == expected_cc
end
end
describe "fetch_ordered_collection" do
import Tesla.Mock
test "fetches the first OrderedCollectionPage when an OrderedCollection is encountered" do
mock(fn
%{method: :get, url: "http://mastodon.com/outbox"} ->
json(%{"type" => "OrderedCollection", "first" => "http://mastodon.com/outbox?page=true"})
%{method: :get, url: "http://mastodon.com/outbox?page=true"} ->
json(%{"type" => "OrderedCollectionPage", "orderedItems" => ["ok"]})
end)
assert Utils.fetch_ordered_collection("http://mastodon.com/outbox", 1) == ["ok"]
end
test "fetches several pages in the right order one after another, but only the specified amount" do
mock(fn
%{method: :get, url: "http://example.com/outbox"} ->
json(%{
"type" => "OrderedCollectionPage",
"orderedItems" => [0],
"next" => "http://example.com/outbox?page=1"
})
%{method: :get, url: "http://example.com/outbox?page=1"} ->
json(%{
"type" => "OrderedCollectionPage",
"orderedItems" => [1],
"next" => "http://example.com/outbox?page=2"
})
%{method: :get, url: "http://example.com/outbox?page=2"} ->
json(%{"type" => "OrderedCollectionPage", "orderedItems" => [2]})
end)
assert Utils.fetch_ordered_collection("http://example.com/outbox", 0) == [0]
assert Utils.fetch_ordered_collection("http://example.com/outbox", 1) == [0, 1]
end
test "returns an error if the url doesn't have an OrderedCollection/Page" do
mock(fn
%{method: :get, url: "http://example.com/not-an-outbox"} ->
json(%{"type" => "NotAnOutbox"})
end)
assert {:error, _} = Utils.fetch_ordered_collection("http://example.com/not-an-outbox", 1)
end
test "returns the what was collected if there are less pages than specified" do
mock(fn
%{method: :get, url: "http://example.com/outbox"} ->
json(%{
"type" => "OrderedCollectionPage",
"orderedItems" => [0],
"next" => "http://example.com/outbox?page=1"
})
%{method: :get, url: "http://example.com/outbox?page=1"} ->
json(%{"type" => "OrderedCollectionPage", "orderedItems" => [1]})
end)
assert Utils.fetch_ordered_collection("http://example.com/outbox", 5) == [0, 1]
end
end
test "make_json_ld_header/0" do
assert Utils.make_json_ld_header() == %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
"@language" => "und"
}
]
}
end
describe "get_existing_votes" do
test "fetches existing votes" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "How do I pronounce LaTeX?",
"poll" => %{
"options" => ["laytekh", "lahtekh", "latex"],
"expires_in" => 20,
"multiple" => true
}
})
object = Object.normalize(activity)
{:ok, votes, object} = CommonAPI.vote(other_user, object, [0, 1])
assert Enum.sort(Utils.get_existing_votes(other_user.ap_id, object)) == Enum.sort(votes)
end
test "fetches only Create activities" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "Are we living in a society?",
"poll" => %{
"options" => ["yes", "no"],
"expires_in" => 20
}
})
object = Object.normalize(activity)
{:ok, [vote], object} = CommonAPI.vote(other_user, object, [0])
vote_object = Object.normalize(vote)
{:ok, _activity, _object} = ActivityPub.like(user, vote_object)
[fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object)
assert fetched_vote.id == vote.id
end
end
describe "update_follow_state_for_all/2" do
test "updates the state of all Follow activities with the same actor and object" do
- user = insert(:user, info: %{locked: true})
+ user = insert(:user, locked: true)
follower = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, user)
{:ok, follow_activity_two} = ActivityPub.follow(follower, user)
data =
follow_activity_two.data
|> Map.put("state", "accept")
cng = Ecto.Changeset.change(follow_activity_two, data: data)
{:ok, follow_activity_two} = Repo.update(cng)
{:ok, follow_activity_two} =
Utils.update_follow_state_for_all(follow_activity_two, "accept")
assert refresh_record(follow_activity).data["state"] == "accept"
assert refresh_record(follow_activity_two).data["state"] == "accept"
end
end
describe "update_follow_state/2" do
test "updates the state of the given follow activity" do
- user = insert(:user, info: %{locked: true})
+ user = insert(:user, locked: true)
follower = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, user)
{:ok, follow_activity_two} = ActivityPub.follow(follower, user)
data =
follow_activity_two.data
|> Map.put("state", "accept")
cng = Ecto.Changeset.change(follow_activity_two, data: data)
{:ok, follow_activity_two} = Repo.update(cng)
{:ok, follow_activity_two} = Utils.update_follow_state(follow_activity_two, "reject")
assert refresh_record(follow_activity).data["state"] == "pending"
assert refresh_record(follow_activity_two).data["state"] == "reject"
end
end
describe "update_element_in_object/3" do
test "updates likes" do
user = insert(:user)
activity = insert(:note_activity)
object = Object.normalize(activity)
assert {:ok, updated_object} =
Utils.update_element_in_object(
"like",
[user.ap_id],
object
)
assert updated_object.data["likes"] == [user.ap_id]
assert updated_object.data["like_count"] == 1
end
end
describe "add_like_to_object/2" do
test "add actor to likes" do
user = insert(:user)
user2 = insert(:user)
object = insert(:note)
assert {:ok, updated_object} =
Utils.add_like_to_object(
%Activity{data: %{"actor" => user.ap_id}},
object
)
assert updated_object.data["likes"] == [user.ap_id]
assert updated_object.data["like_count"] == 1
assert {:ok, updated_object2} =
Utils.add_like_to_object(
%Activity{data: %{"actor" => user2.ap_id}},
updated_object
)
assert updated_object2.data["likes"] == [user2.ap_id, user.ap_id]
assert updated_object2.data["like_count"] == 2
end
end
describe "remove_like_from_object/2" do
test "removes ap_id from likes" do
user = insert(:user)
user2 = insert(:user)
object = insert(:note, data: %{"likes" => [user.ap_id, user2.ap_id], "like_count" => 2})
assert {:ok, updated_object} =
Utils.remove_like_from_object(
%Activity{data: %{"actor" => user.ap_id}},
object
)
assert updated_object.data["likes"] == [user2.ap_id]
assert updated_object.data["like_count"] == 1
end
end
describe "get_existing_like/2" do
test "fetches existing like" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity)
user = insert(:user)
refute Utils.get_existing_like(user.ap_id, object)
{:ok, like_activity, _object} = ActivityPub.like(user, object)
assert ^like_activity = Utils.get_existing_like(user.ap_id, object)
end
end
describe "get_get_existing_announce/2" do
test "returns nil if announce not found" do
actor = insert(:user)
refute Utils.get_existing_announce(actor.ap_id, %{data: %{"id" => "test"}})
end
test "fetches existing announce" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity)
actor = insert(:user)
{:ok, announce, _object} = ActivityPub.announce(actor, object)
assert Utils.get_existing_announce(actor.ap_id, object) == announce
end
end
describe "fetch_latest_block/2" do
test "fetches last block activities" do
user1 = insert(:user)
user2 = insert(:user)
assert {:ok, %Activity{} = _} = ActivityPub.block(user1, user2)
assert {:ok, %Activity{} = _} = ActivityPub.block(user1, user2)
assert {:ok, %Activity{} = activity} = ActivityPub.block(user1, user2)
assert Utils.fetch_latest_block(user1, user2) == activity
end
end
describe "recipient_in_message/3" do
test "returns true when recipient in `to`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"to" => [recipient.ap_id], "cc" => ""}
)
end
test "returns true when recipient in `cc`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"cc" => [recipient.ap_id], "to" => ""}
)
end
test "returns true when recipient in `bto`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"bcc" => "", "bto" => [recipient.ap_id]}
)
end
test "returns true when recipient in `bcc`" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"bto" => "", "bcc" => [recipient.ap_id]}
)
end
test "returns true when message without addresses fields" do
recipient = insert(:user)
actor = insert(:user)
assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id})
assert Utils.recipient_in_message(
recipient,
actor,
%{"btod" => "", "bccc" => [recipient.ap_id]}
)
end
test "returns false" do
recipient = insert(:user)
actor = insert(:user)
refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"})
end
end
describe "lazy_put_activity_defaults/2" do
test "returns map with id and published data" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
res = Utils.lazy_put_activity_defaults(%{"context" => object.data["id"]})
assert res["context"] == object.data["id"]
assert res["context_id"] == object.id
assert res["id"]
assert res["published"]
end
test "returns map with fake id and published data" do
assert %{
"context" => "pleroma:fakecontext",
"context_id" => -1,
"id" => "pleroma:fakeid",
"published" => _
} = Utils.lazy_put_activity_defaults(%{}, true)
end
test "returns activity data with object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
res =
Utils.lazy_put_activity_defaults(%{
"context" => object.data["id"],
"object" => %{}
})
assert res["context"] == object.data["id"]
assert res["context_id"] == object.id
assert res["id"]
assert res["published"]
assert res["object"]["id"]
assert res["object"]["published"]
assert res["object"]["context"] == object.data["id"]
assert res["object"]["context_id"] == object.id
end
end
describe "make_flag_data" do
test "returns empty map when params is invalid" do
assert Utils.make_flag_data(%{}, %{}) == %{}
end
test "returns map with Flag object" do
reporter = insert(:user)
target_account = insert(:user)
{:ok, activity} = CommonAPI.post(target_account, %{"status" => "foobar"})
context = Utils.generate_context_id()
content = "foobar"
target_ap_id = target_account.ap_id
activity_ap_id = activity.data["id"]
res =
Utils.make_flag_data(
%{
actor: reporter,
context: context,
account: target_account,
statuses: [%{"id" => activity.data["id"]}],
content: content
},
%{}
)
note_obj = %{
"type" => "Note",
"id" => activity_ap_id,
"content" => content,
"published" => activity.object.data["published"],
"actor" => AccountView.render("show.json", %{user: target_account})
}
assert %{
"type" => "Flag",
"content" => ^content,
"context" => ^context,
"object" => [^target_ap_id, ^note_obj],
"state" => "open"
} = res
end
end
describe "add_announce_to_object/2" do
test "adds actor to announcement" do
user = insert(:user)
object = insert(:note)
activity =
insert(:note_activity,
data: %{
"actor" => user.ap_id,
"cc" => [Pleroma.Constants.as_public()]
}
)
assert {:ok, updated_object} = Utils.add_announce_to_object(activity, object)
assert updated_object.data["announcements"] == [user.ap_id]
assert updated_object.data["announcement_count"] == 1
end
end
describe "remove_announce_from_object/2" do
test "removes actor from announcements" do
user = insert(:user)
user2 = insert(:user)
object =
insert(:note,
data: %{"announcements" => [user.ap_id, user2.ap_id], "announcement_count" => 2}
)
activity = insert(:note_activity, data: %{"actor" => user.ap_id})
assert {:ok, updated_object} = Utils.remove_announce_from_object(activity, object)
assert updated_object.data["announcements"] == [user2.ap_id]
assert updated_object.data["announcement_count"] == 1
end
end
end
diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs
index 3155749aa..3299be2d5 100644
--- a/test/web/activity_pub/views/user_view_test.exs
+++ b/test/web/activity_pub/views/user_view_test.exs
@@ -1,192 +1,192 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UserViewTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.User
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.CommonAPI
test "Renders a user, including the public key" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
assert result["id"] == user.ap_id
assert result["preferredUsername"] == user.nickname
assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY")
end
test "Renders profile fields" do
fields = [
%{"name" => "foo", "value" => "bar"}
]
{:ok, user} =
insert(:user)
- |> User.upgrade_changeset(%{info: %{fields: fields}})
+ |> User.upgrade_changeset(%{fields: fields})
|> User.update_and_set_cache()
assert %{
"attachment" => [%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}]
} = UserView.render("user.json", %{user: user})
end
test "Renders with emoji tags" do
- user = insert(:user, %{info: %{emoji: [%{"bib" => "/test"}]}})
+ user = insert(:user, emoji: [%{"bib" => "/test"}])
assert %{
"tag" => [
%{
"icon" => %{"type" => "Image", "url" => "/test"},
"id" => "/test",
"name" => ":bib:",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z"
}
]
} = UserView.render("user.json", %{user: user})
end
test "Does not add an avatar image if the user hasn't set one" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
refute result["icon"]
refute result["image"]
user =
insert(:user,
avatar: %{"url" => [%{"href" => "https://someurl"}]},
- info: %{
- banner: %{"url" => [%{"href" => "https://somebanner"}]}
- }
+ banner: %{"url" => [%{"href" => "https://somebanner"}]}
)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
assert result["icon"]["url"] == "https://someurl"
assert result["image"]["url"] == "https://somebanner"
end
+ test "renders an invisible user with the invisible property set to true" do
+ user = insert(:user, invisible: true)
+
+ assert %{"invisible" => true} = UserView.render("service.json", %{user: user})
+ end
+
describe "endpoints" do
test "local users have a usable endpoints structure" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
assert result["id"] == user.ap_id
%{
"sharedInbox" => _,
"oauthAuthorizationEndpoint" => _,
"oauthRegistrationEndpoint" => _,
"oauthTokenEndpoint" => _
} = result["endpoints"]
end
test "remote users have an empty endpoints structure" do
user = insert(:user, local: false)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
assert result["id"] == user.ap_id
assert result["endpoints"] == %{}
end
test "instance users do not expose oAuth endpoints" do
user = insert(:user, nickname: nil, local: true)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
refute result["endpoints"]["oauthAuthorizationEndpoint"]
refute result["endpoints"]["oauthRegistrationEndpoint"]
refute result["endpoints"]["oauthTokenEndpoint"]
end
end
describe "followers" do
test "sets totalItems to zero when followers are hidden" do
user = insert(:user)
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
- info = Map.merge(user.info, %{hide_followers_count: true, hide_followers: true})
- user = Map.put(user, :info, info)
+ user = Map.merge(user, %{hide_followers_count: true, hide_followers: true})
assert %{"totalItems" => 0} = UserView.render("followers.json", %{user: user})
end
test "sets correct totalItems when followers are hidden but the follower counter is not" do
user = insert(:user)
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
- info = Map.merge(user.info, %{hide_followers_count: false, hide_followers: true})
- user = Map.put(user, :info, info)
+ user = Map.merge(user, %{hide_followers_count: false, hide_followers: true})
assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
end
end
describe "following" do
test "sets totalItems to zero when follows are hidden" do
user = insert(:user)
other_user = insert(:user)
{:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
- info = Map.merge(user.info, %{hide_follows_count: true, hide_follows: true})
- user = Map.put(user, :info, info)
+ user = Map.merge(user, %{hide_follows_count: true, hide_follows: true})
assert %{"totalItems" => 0} = UserView.render("following.json", %{user: user})
end
test "sets correct totalItems when follows are hidden but the follow counter is not" do
user = insert(:user)
other_user = insert(:user)
{:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
- info = Map.merge(user.info, %{hide_follows_count: false, hide_follows: true})
- user = Map.put(user, :info, info)
+ user = Map.merge(user, %{hide_follows_count: false, hide_follows: true})
assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
end
end
test "activity collection page aginates correctly" do
user = insert(:user)
posts =
for i <- 0..25 do
{:ok, activity} = CommonAPI.post(user, %{"status" => "post #{i}"})
activity
end
# outbox sorts chronologically, newest first, with ten per page
posts = Enum.reverse(posts)
%{"next" => next_url} =
UserView.render("activity_collection_page.json", %{
iri: "#{user.ap_id}/outbox",
activities: Enum.take(posts, 10)
})
next_id = Enum.at(posts, 9).id
assert next_url =~ next_id
%{"next" => next_url} =
UserView.render("activity_collection_page.json", %{
iri: "#{user.ap_id}/outbox",
activities: Enum.take(Enum.drop(posts, 10), 10)
})
next_id = Enum.at(posts, 19).id
assert next_url =~ next_id
end
end
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index b5c355e66..58435d23c 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -1,2496 +1,2625 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MediaProxy
import Pleroma.Factory
- describe "/api/pleroma/admin/users" do
- test "Delete" do
- admin = insert(:user, info: %{is_admin: true})
+ setup_all do
+ Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+ :ok
+ end
+
+ describe "DELETE /api/pleroma/admin/users" do
+ test "single user" do
+ admin = insert(:user, is_admin: true)
user = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
log_entry = Repo.one(ModerationLog)
- assert log_entry.data["subject"]["nickname"] == user.nickname
- assert log_entry.data["action"] == "delete"
-
assert ModerationLog.get_log_entry_message(log_entry) ==
- "@#{admin.nickname} deleted user @#{user.nickname}"
+ "@#{admin.nickname} deleted users: @#{user.nickname}"
assert json_response(conn, 200) == user.nickname
end
+ test "multiple users" do
+ admin = insert(:user, is_admin: true)
+ user_one = insert(:user)
+ user_two = insert(:user)
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> put_req_header("accept", "application/json")
+ |> delete("/api/pleroma/admin/users", %{
+ nicknames: [user_one.nickname, user_two.nickname]
+ })
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}"
+
+ response = json_response(conn, 200)
+ assert response -- [user_one.nickname, user_two.nickname] == []
+ end
+ end
+
+ describe "/api/pleroma/admin/users" do
test "Create" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => "lain@example.org",
"password" => "test"
},
%{
"nickname" => "lain2",
"email" => "lain2@example.org",
"password" => "test"
}
]
})
response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type"))
assert response == ["success", "success"]
log_entry = Repo.one(ModerationLog)
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
end
test "Cannot create user with exisiting email" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => user.email,
"nickname" => "lain"
},
"error" => "email has already been taken",
"type" => "error"
}
]
end
test "Cannot create user with exisiting nickname" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => user.nickname,
"email" => "someuser@plerama.social",
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => "someuser@plerama.social",
"nickname" => user.nickname
},
"error" => "nickname has already been taken",
"type" => "error"
}
]
end
test "Multiple user creation works in transaction" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "newuser",
"email" => "newuser@pleroma.social",
"password" => "test"
},
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => user.email,
"nickname" => "lain"
},
"error" => "email has already been taken",
"type" => "error"
},
%{
"code" => 409,
"data" => %{
"email" => "newuser@pleroma.social",
"nickname" => "newuser"
},
"error" => "",
"type" => "error"
}
]
assert User.get_by_nickname("newuser") === nil
end
end
describe "/api/pleroma/admin/users/:nickname" do
test "Show", %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = insert(:user)
conn =
conn
|> assign(:user, admin)
|> get("/api/pleroma/admin/users/#{user.nickname}")
expected = %{
"deactivated" => false,
"id" => to_string(user.id),
"local" => true,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
}
assert expected == json_response(conn, 200)
end
test "when the user doesn't exist", %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = build(:user)
conn =
conn
|> assign(:user, admin)
|> get("/api/pleroma/admin/users/#{user.nickname}")
assert "Not found" == json_response(conn, 404)
end
end
describe "/api/pleroma/admin/users/follow" do
test "allows to force-follow another user" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = insert(:user)
follower = insert(:user)
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/follow", %{
"follower" => follower.nickname,
"followed" => user.nickname
})
user = User.get_cached_by_id(user.id)
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, user)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}"
end
end
describe "/api/pleroma/admin/users/unfollow" do
test "allows to force-unfollow another user" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = insert(:user)
follower = insert(:user)
User.follow(follower, user)
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/unfollow", %{
"follower" => follower.nickname,
"followed" => user.nickname
})
user = User.get_cached_by_id(user.id)
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, user)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}"
end
end
describe "PUT /api/pleroma/admin/users/tag" do
setup do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y"]})
user3 = insert(:user, %{tags: ["unchanged"]})
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> put(
"/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=#{
user2.nickname
}&tags[]=foo&tags[]=bar"
)
%{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3}
end
test "it appends specified tags to users with specified nicknames", %{
conn: conn,
admin: admin,
user1: user1,
user2: user2
} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"]
assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"]
log_entry = Repo.one(ModerationLog)
users =
[user1.nickname, user2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["foo", "bar"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} added tags: #{tags} to users: #{users}"
end
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user3.id).tags == ["unchanged"]
end
end
describe "DELETE /api/pleroma/admin/users/tag" do
setup do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user1 = insert(:user, %{tags: ["x"]})
user2 = insert(:user, %{tags: ["y", "z"]})
user3 = insert(:user, %{tags: ["unchanged"]})
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> delete(
"/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=#{
user2.nickname
}&tags[]=x&tags[]=z"
)
%{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3}
end
test "it removes specified tags from users with specified nicknames", %{
conn: conn,
admin: admin,
user1: user1,
user2: user2
} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user1.id).tags == []
assert User.get_cached_by_id(user2.id).tags == ["y"]
log_entry = Repo.one(ModerationLog)
users =
[user1.nickname, user2.nickname]
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags = ["x", "z"] |> Enum.join(", ")
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} removed tags: #{tags} from users: #{users}"
end
test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do
assert json_response(conn, :no_content)
assert User.get_cached_by_id(user3.id).tags == ["unchanged"]
end
end
describe "/api/pleroma/admin/users/:nickname/permission_group" do
test "GET is giving user_info" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/")
assert json_response(conn, 200) == %{
"is_admin" => true,
"is_moderator" => false
}
end
test "/:right POST, can add to a permission group" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
assert json_response(conn, 200) == %{
"is_admin" => true
}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{user.nickname} admin"
end
- test "/:right DELETE, can remove from a permission group" do
- admin = insert(:user, info: %{is_admin: true})
- user = insert(:user, info: %{is_admin: true})
+ test "/:right POST, can add to a permission group (multiple)" do
+ admin = insert(:user, is_admin: true)
+ user_one = insert(:user)
+ user_two = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
- |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
+ |> post("/api/pleroma/admin/users/permission_group/admin", %{
+ nicknames: [user_one.nickname, user_two.nickname]
+ })
assert json_response(conn, 200) == %{
- "is_admin" => false
+ "is_admin" => true
}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
- "@#{admin.nickname} revoked admin role from @#{user.nickname}"
+ "@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
end
- end
- describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do
- setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ test "/:right DELETE, can remove from a permission group" do
+ admin = insert(:user, is_admin: true)
+ user = insert(:user, is_admin: true)
conn =
- conn
+ build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
+ |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
- %{conn: conn, admin: admin}
- end
-
- test "deactivates the user", %{conn: conn, admin: admin} do
- user = insert(:user)
-
- conn =
- conn
- |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
-
- user = User.get_cached_by_id(user.id)
- assert user.info.deactivated == true
- assert json_response(conn, :no_content)
+ assert json_response(conn, 200) == %{
+ "is_admin" => false
+ }
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
- "@#{admin.nickname} deactivated user @#{user.nickname}"
+ "@#{admin.nickname} revoked admin role from @#{user.nickname}"
end
- test "activates the user", %{conn: conn, admin: admin} do
- user = insert(:user, info: %{deactivated: true})
+ test "/:right DELETE, can remove from a permission group (multiple)" do
+ admin = insert(:user, is_admin: true)
+ user_one = insert(:user, is_admin: true)
+ user_two = insert(:user, is_admin: true)
conn =
- conn
- |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true})
+ build_conn()
+ |> assign(:user, admin)
+ |> put_req_header("accept", "application/json")
+ |> delete("/api/pleroma/admin/users/permission_group/admin", %{
+ nicknames: [user_one.nickname, user_two.nickname]
+ })
- user = User.get_cached_by_id(user.id)
- assert user.info.deactivated == false
- assert json_response(conn, :no_content)
+ assert json_response(conn, 200) == %{
+ "is_admin" => false
+ }
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
- "@#{admin.nickname} activated user @#{user.nickname}"
- end
-
- test "returns 403 when requested by a non-admin", %{conn: conn} do
- user = insert(:user)
-
- conn =
- conn
- |> assign(:user, user)
- |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
-
- assert json_response(conn, :forbidden)
+ "@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{
+ user_two.nickname
+ }"
end
end
describe "POST /api/pleroma/admin/email_invite, with valid config" do
setup do
- [user: insert(:user, info: %{is_admin: true})]
+ [user: insert(:user, is_admin: true)]
end
clear_config([:instance, :registrations_open]) do
Pleroma.Config.put([:instance, :registrations_open], false)
end
clear_config([:instance, :invites_enabled]) do
Pleroma.Config.put([:instance, :invites_enabled], true)
end
test "sends invitation and returns 204", %{conn: conn, user: user} do
recipient_email = "foo@bar.com"
recipient_name = "J. D."
conn =
conn
|> assign(:user, user)
|> post(
"/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}"
)
assert json_response(conn, :no_content)
token_record = List.last(Pleroma.Repo.all(Pleroma.UserInviteToken))
assert token_record
refute token_record.used
notify_email = Pleroma.Config.get([:instance, :notify_email])
instance_name = Pleroma.Config.get([:instance, :name])
email =
Pleroma.Emails.UserEmail.user_invitation_email(
user,
token_record,
recipient_email,
recipient_name
)
Swoosh.TestAssertions.assert_email_sent(
from: {instance_name, notify_email},
to: {recipient_name, recipient_email},
html_body: email.html_body
)
end
test "it returns 403 if requested by a non-admin", %{conn: conn} do
non_admin_user = insert(:user)
conn =
conn
|> assign(:user, non_admin_user)
|> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :forbidden)
end
end
describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do
setup do
- [user: insert(:user, info: %{is_admin: true})]
+ [user: insert(:user, is_admin: true)]
end
clear_config([:instance, :registrations_open])
clear_config([:instance, :invites_enabled])
test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: user} do
Pleroma.Config.put([:instance, :registrations_open], false)
Pleroma.Config.put([:instance, :invites_enabled], false)
conn =
conn
|> assign(:user, user)
|> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: user} do
Pleroma.Config.put([:instance, :registrations_open], true)
Pleroma.Config.put([:instance, :invites_enabled], true)
conn =
conn
|> assign(:user, user)
|> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
end
test "/api/pleroma/admin/users/:nickname/password_reset" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{user.nickname}/password_reset")
resp = json_response(conn, 200)
assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"])
end
describe "GET /api/pleroma/admin/users" do
setup do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
conn =
build_conn()
|> assign(:user, admin)
{:ok, conn: conn, admin: admin}
end
test "renders users array for the first page", %{conn: conn, admin: admin} do
user = insert(:user, local: false, tags: ["foo", "bar"])
conn = get(conn, "/api/pleroma/admin/users?page=1")
users =
[
%{
- "deactivated" => admin.info.deactivated,
+ "deactivated" => admin.deactivated,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname)
},
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => false,
"tags" => ["foo", "bar"],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "renders empty array for the second page", %{conn: conn} do
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?page=2")
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => []
}
end
test "regular search", %{conn: conn} do
user = insert(:user, nickname: "bob")
conn = get(conn, "/api/pleroma/admin/users?query=bo")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
}
]
}
end
test "search by domain", %{conn: conn} do
user = insert(:user, nickname: "nickname@domain.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?query=domain.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
}
]
}
end
test "search by full nickname", %{conn: conn} do
user = insert(:user, nickname: "nickname@domain.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
}
]
}
end
test "search by display name", %{conn: conn} do
user = insert(:user, name: "Display name")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?name=display")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
}
]
}
end
test "search by email", %{conn: conn} do
user = insert(:user, email: "email@example.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?email=email@example.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
}
]
}
end
test "regular search with page size", %{conn: conn} do
user = insert(:user, nickname: "aalice")
user2 = insert(:user, nickname: "alice")
conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1")
assert json_response(conn1, 200) == %{
"count" => 2,
"page_size" => 1,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
}
]
}
conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2")
assert json_response(conn2, 200) == %{
"count" => 2,
"page_size" => 1,
"users" => [
%{
- "deactivated" => user2.info.deactivated,
+ "deactivated" => user2.deactivated,
"id" => user2.id,
"nickname" => user2.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user2.name || user2.nickname)
}
]
}
end
test "only local users" do
- admin = insert(:user, info: %{is_admin: true}, nickname: "john")
+ admin = insert(:user, is_admin: true, nickname: "john")
user = insert(:user, nickname: "bob")
insert(:user, nickname: "bobb", local: false)
conn =
build_conn()
|> assign(:user, admin)
|> get("/api/pleroma/admin/users?query=bo&filters=local")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
}
]
}
end
test "only local users with no query", %{admin: old_admin} do
- admin = insert(:user, info: %{is_admin: true}, nickname: "john")
+ admin = insert(:user, is_admin: true, nickname: "john")
user = insert(:user, nickname: "bob")
insert(:user, nickname: "bobb", local: false)
conn =
build_conn()
|> assign(:user, admin)
|> get("/api/pleroma/admin/users?filters=local")
users =
[
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
},
%{
- "deactivated" => admin.info.deactivated,
+ "deactivated" => admin.deactivated,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname)
},
%{
"deactivated" => false,
"id" => old_admin.id,
"local" => true,
"nickname" => old_admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"tags" => [],
"avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname)
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 3,
"page_size" => 50,
"users" => users
}
end
test "load only admins", %{conn: conn, admin: admin} do
- second_admin = insert(:user, info: %{is_admin: true})
+ second_admin = insert(:user, is_admin: true)
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?filters=is_admin")
users =
[
%{
"deactivated" => false,
"id" => admin.id,
"nickname" => admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => admin.local,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname)
},
%{
"deactivated" => false,
"id" => second_admin.id,
"nickname" => second_admin.nickname,
"roles" => %{"admin" => true, "moderator" => false},
"local" => second_admin.local,
"tags" => [],
"avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname)
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "load only moderators", %{conn: conn} do
- moderator = insert(:user, info: %{is_moderator: true})
+ moderator = insert(:user, is_moderator: true)
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
"deactivated" => false,
"id" => moderator.id,
"nickname" => moderator.nickname,
"roles" => %{"admin" => false, "moderator" => true},
"local" => moderator.local,
"tags" => [],
"avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(moderator.name || moderator.nickname)
}
]
}
end
test "load users with tags list", %{conn: conn} do
user1 = insert(:user, tags: ["first"])
user2 = insert(:user, tags: ["second"])
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second")
users =
[
%{
"deactivated" => false,
"id" => user1.id,
"nickname" => user1.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => user1.local,
"tags" => ["first"],
"avatar" => User.avatar_url(user1) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user1.name || user1.nickname)
},
%{
"deactivated" => false,
"id" => user2.id,
"nickname" => user2.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => user2.local,
"tags" => ["second"],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user2.name || user2.nickname)
}
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "it works with multiple filters" do
- admin = insert(:user, nickname: "john", info: %{is_admin: true})
- user = insert(:user, nickname: "bob", local: false, info: %{deactivated: true})
+ admin = insert(:user, nickname: "john", is_admin: true)
+ user = insert(:user, nickname: "bob", local: false, deactivated: true)
- insert(:user, nickname: "ken", local: true, info: %{deactivated: true})
- insert(:user, nickname: "bobb", local: false, info: %{deactivated: false})
+ insert(:user, nickname: "ken", local: true, deactivated: true)
+ insert(:user, nickname: "bobb", local: false, deactivated: false)
conn =
build_conn()
|> assign(:user, admin)
|> get("/api/pleroma/admin/users?filters=deactivated,external")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
%{
- "deactivated" => user.info.deactivated,
+ "deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => user.local,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
}
]
}
end
end
+ test "PATCH /api/pleroma/admin/users/activate" do
+ admin = insert(:user, is_admin: true)
+ user_one = insert(:user, deactivated: true)
+ user_two = insert(:user, deactivated: true)
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> patch(
+ "/api/pleroma/admin/users/activate",
+ %{nicknames: [user_one.nickname, user_two.nickname]}
+ )
+
+ response = json_response(conn, 200)
+ assert Enum.map(response["users"], & &1["deactivated"]) == [false, false]
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}"
+ end
+
+ test "PATCH /api/pleroma/admin/users/deactivate" do
+ admin = insert(:user, is_admin: true)
+ user_one = insert(:user, deactivated: false)
+ user_two = insert(:user, deactivated: false)
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> patch(
+ "/api/pleroma/admin/users/deactivate",
+ %{nicknames: [user_one.nickname, user_two.nickname]}
+ )
+
+ response = json_response(conn, 200)
+ assert Enum.map(response["users"], & &1["deactivated"]) == [true, true]
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
+ end
+
test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = insert(:user)
conn =
build_conn()
|> assign(:user, admin)
|> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
assert json_response(conn, 200) ==
%{
- "deactivated" => !user.info.deactivated,
+ "deactivated" => !user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
}
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
- "@#{admin.nickname} deactivated user @#{user.nickname}"
+ "@#{admin.nickname} deactivated users: @#{user.nickname}"
end
describe "POST /api/pleroma/admin/users/invite_token" do
setup do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
conn =
build_conn()
|> assign(:user, admin)
{:ok, conn: conn}
end
test "without options", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/invite_token")
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
refute invite.expires_at
refute invite.max_use
assert invite.invite_type == "one_time"
end
test "with expires_at", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/users/invite_token", %{
"expires_at" => Date.to_string(Date.utc_today())
})
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
assert invite.expires_at == Date.utc_today()
refute invite.max_use
assert invite.invite_type == "date_limited"
end
test "with max_use", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150})
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
refute invite.expires_at
assert invite.max_use == 150
assert invite.invite_type == "reusable"
end
test "with max use and expires_at", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/users/invite_token", %{
"max_use" => 150,
"expires_at" => Date.to_string(Date.utc_today())
})
invite_json = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
assert invite.expires_at == Date.utc_today()
assert invite.max_use == 150
assert invite.invite_type == "reusable_date_limited"
end
end
describe "GET /api/pleroma/admin/users/invites" do
setup do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
conn =
build_conn()
|> assign(:user, admin)
{:ok, conn: conn}
end
test "no invites", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/users/invites")
assert json_response(conn, 200) == %{"invites" => []}
end
test "with invite", %{conn: conn} do
{:ok, invite} = UserInviteToken.create_invite()
conn = get(conn, "/api/pleroma/admin/users/invites")
assert json_response(conn, 200) == %{
"invites" => [
%{
"expires_at" => nil,
"id" => invite.id,
"invite_type" => "one_time",
"max_use" => nil,
"token" => invite.token,
"used" => false,
"uses" => 0
}
]
}
end
end
describe "POST /api/pleroma/admin/users/revoke_invite" do
test "with token" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
{:ok, invite} = UserInviteToken.create_invite()
conn =
build_conn()
|> assign(:user, admin)
|> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
assert json_response(conn, 200) == %{
"expires_at" => nil,
"id" => invite.id,
"invite_type" => "one_time",
"max_use" => nil,
"token" => invite.token,
"used" => true,
"uses" => 0
}
end
test "with invalid token" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
conn =
build_conn()
|> assign(:user, admin)
|> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"})
assert json_response(conn, :not_found) == "Not found"
end
end
describe "GET /api/pleroma/admin/reports/:id" do
setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
%{conn: assign(conn, :user, admin)}
end
test "returns report by its id", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
response =
conn
|> get("/api/pleroma/admin/reports/#{report_id}")
|> json_response(:ok)
assert response["id"] == report_id
end
test "returns 404 when report id is invalid", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/reports/test")
assert json_response(conn, :not_found) == "Not found"
end
end
describe "PUT /api/pleroma/admin/reports/:id" do
setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
%{conn: assign(conn, :user, admin), id: report_id, admin: admin}
end
test "mark report as resolved", %{conn: conn, id: id, admin: admin} do
response =
conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"})
|> json_response(:ok)
assert response["state"] == "resolved"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'resolved' state"
end
test "closes report", %{conn: conn, id: id, admin: admin} do
response =
conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"})
|> json_response(:ok)
assert response["state"] == "closed"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated report ##{id} with 'closed' state"
end
test "returns 400 when state is unknown", %{conn: conn, id: id} do
conn =
conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "test"})
assert json_response(conn, :bad_request) == "Unsupported state"
end
test "returns 404 when report is not exist", %{conn: conn} do
conn =
conn
|> put("/api/pleroma/admin/reports/test", %{"state" => "closed"})
assert json_response(conn, :not_found) == "Not found"
end
end
describe "GET /api/pleroma/admin/reports" do
setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
%{conn: assign(conn, :user, admin)}
end
test "returns empty response when no reports created", %{conn: conn} do
response =
conn
|> get("/api/pleroma/admin/reports")
|> json_response(:ok)
assert Enum.empty?(response["reports"])
assert response["total"] == 0
end
test "returns reports", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
response =
conn
|> get("/api/pleroma/admin/reports")
|> json_response(:ok)
[report] = response["reports"]
assert length(response["reports"]) == 1
assert report["id"] == report_id
assert response["total"] == 1
end
test "returns reports with specified state", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: first_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
{:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I don't like this user"
})
CommonAPI.update_report_state(second_report_id, "closed")
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "open"
})
|> json_response(:ok)
[open_report] = response["reports"]
assert length(response["reports"]) == 1
assert open_report["id"] == first_report_id
assert response["total"] == 1
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "closed"
})
|> json_response(:ok)
[closed_report] = response["reports"]
assert length(response["reports"]) == 1
assert closed_report["id"] == second_report_id
assert response["total"] == 1
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "resolved"
})
|> json_response(:ok)
assert Enum.empty?(response["reports"])
assert response["total"] == 0
end
test "returns 403 when requested by a non-admin" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
|> get("/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "User is not admin."}
end
test "returns 403 when requested by anonymous" do
conn =
build_conn()
|> get("/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."}
end
end
#
describe "POST /api/pleroma/admin/reports/:id/respond" do
setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
%{conn: assign(conn, :user, admin), admin: admin}
end
test "returns created dm", %{conn: conn, admin: admin} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
response =
conn
|> post("/api/pleroma/admin/reports/#{report_id}/respond", %{
"status" => "I will check it out"
})
|> json_response(:ok)
recipients = Enum.map(response["mentions"], & &1["username"])
assert reporter.nickname in recipients
assert response["content"] == "I will check it out"
assert response["visibility"] == "direct"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} responded with 'I will check it out' to report ##{
response["id"]
}"
end
test "returns 400 when status is missing", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/reports/test/respond")
assert json_response(conn, :bad_request) == "Invalid parameters"
end
test "returns 404 when report id is invalid", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/reports/test/respond", %{
"status" => "foo"
})
assert json_response(conn, :not_found) == "Not found"
end
end
describe "PUT /api/pleroma/admin/statuses/:id" do
setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
activity = insert(:note_activity)
%{conn: assign(conn, :user, admin), id: activity.id, admin: admin}
end
test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"})
|> json_response(:ok)
assert response["sensitive"]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated status ##{id}, set sensitive: 'true'"
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"})
|> json_response(:ok)
refute response["sensitive"]
end
test "change visibility flag", %{conn: conn, id: id, admin: admin} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"})
|> json_response(:ok)
assert response["visibility"] == "public"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} updated status ##{id}, set visibility: 'public'"
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"})
|> json_response(:ok)
assert response["visibility"] == "private"
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "unlisted"})
|> json_response(:ok)
assert response["visibility"] == "unlisted"
end
test "returns 400 when visibility is unknown", %{conn: conn, id: id} do
conn =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"})
assert json_response(conn, :bad_request) == "Unsupported visibility"
end
end
describe "DELETE /api/pleroma/admin/statuses/:id" do
setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
activity = insert(:note_activity)
%{conn: assign(conn, :user, admin), id: activity.id, admin: admin}
end
test "deletes status", %{conn: conn, id: id, admin: admin} do
conn
|> delete("/api/pleroma/admin/statuses/#{id}")
|> json_response(:ok)
refute Activity.get_by_id(id)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted status ##{id}"
end
test "returns error when status is not exist", %{conn: conn} do
conn =
conn
|> delete("/api/pleroma/admin/statuses/test")
assert json_response(conn, :bad_request) == "Could not delete"
end
end
describe "GET /api/pleroma/admin/config" do
setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
%{conn: assign(conn, :user, admin)}
end
test "without any settings in db", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/config")
assert json_response(conn, 200) == %{"configs" => []}
end
test "with settings in db", %{conn: conn} do
config1 = insert(:config)
config2 = insert(:config)
conn = get(conn, "/api/pleroma/admin/config")
%{
"configs" => [
%{
"key" => key1,
"value" => _
},
%{
"key" => key2,
"value" => _
}
]
} = json_response(conn, 200)
assert key1 == config1.key
assert key2 == config2.key
end
end
describe "POST /api/pleroma/admin/config" do
setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
temp_file = "config/test.exported_from_db.secret.exs"
on_exit(fn ->
Application.delete_env(:pleroma, :key1)
Application.delete_env(:pleroma, :key2)
Application.delete_env(:pleroma, :key3)
Application.delete_env(:pleroma, :key4)
Application.delete_env(:pleroma, :keyaa1)
Application.delete_env(:pleroma, :keyaa2)
Application.delete_env(:pleroma, Pleroma.Web.Endpoint.NotReal)
Application.delete_env(:pleroma, Pleroma.Captcha.NotReal)
:ok = File.rm(temp_file)
end)
%{conn: assign(conn, :user, admin)}
end
clear_config([:instance, :dynamic_configuration]) do
Pleroma.Config.put([:instance, :dynamic_configuration], true)
end
test "create new config setting in db", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: "pleroma", key: "key1", value: "value1"},
%{
group: "ueberauth",
key: "Ueberauth.Strategy.Twitter.OAuth",
value: [%{"tuple" => [":consumer_secret", "aaaa"]}]
},
%{
group: "pleroma",
key: "key2",
value: %{
":nested_1" => "nested_value1",
":nested_2" => [
%{":nested_22" => "nested_value222"},
%{":nested_33" => %{":nested_44" => "nested_444"}}
]
}
},
%{
group: "pleroma",
key: "key3",
value: [
%{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
%{"nested_4" => true}
]
},
%{
group: "pleroma",
key: "key4",
value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"}
},
%{
group: "idna",
key: "key5",
value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => "pleroma",
"key" => "key1",
"value" => "value1"
},
%{
"group" => "ueberauth",
"key" => "Ueberauth.Strategy.Twitter.OAuth",
"value" => [%{"tuple" => [":consumer_secret", "aaaa"]}]
},
%{
"group" => "pleroma",
"key" => "key2",
"value" => %{
":nested_1" => "nested_value1",
":nested_2" => [
%{":nested_22" => "nested_value222"},
%{":nested_33" => %{":nested_44" => "nested_444"}}
]
}
},
%{
"group" => "pleroma",
"key" => "key3",
"value" => [
%{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
%{"nested_4" => true}
]
},
%{
"group" => "pleroma",
"key" => "key4",
"value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"}
},
%{
"group" => "idna",
"key" => "key5",
"value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
}
]
}
assert Application.get_env(:pleroma, :key1) == "value1"
assert Application.get_env(:pleroma, :key2) == %{
nested_1: "nested_value1",
nested_2: [
%{nested_22: "nested_value222"},
%{nested_33: %{nested_44: "nested_444"}}
]
}
assert Application.get_env(:pleroma, :key3) == [
%{"nested_3" => :nested_3, "nested_33" => "nested_33"},
%{"nested_4" => true}
]
assert Application.get_env(:pleroma, :key4) == %{
"endpoint" => "https://example.com",
nested_5: :upload
}
assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
end
test "update config setting & delete", %{conn: conn} do
config1 = insert(:config, key: "keyaa1")
config2 = insert(:config, key: "keyaa2")
insert(:config,
group: "ueberauth",
key: "Ueberauth.Strategy.Microsoft.OAuth",
value: :erlang.term_to_binary([])
)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: config1.group, key: config1.key, value: "another_value"},
%{group: config2.group, key: config2.key, delete: "true"},
%{
group: "ueberauth",
key: "Ueberauth.Strategy.Microsoft.OAuth",
delete: "true"
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => "pleroma",
"key" => config1.key,
"value" => "another_value"
}
]
}
assert Application.get_env(:pleroma, :keyaa1) == "another_value"
refute Application.get_env(:pleroma, :keyaa2)
end
test "common config example", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => "pleroma",
"key" => "Pleroma.Captcha.NotReal",
"value" => [
%{"tuple" => [":enabled", false]},
%{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]},
%{"tuple" => [":seconds_valid", 60]},
%{"tuple" => [":path", ""]},
%{"tuple" => [":key1", nil]},
%{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]},
%{"tuple" => [":regex1", "~r/https:\/\/example.com/"]},
%{"tuple" => [":regex2", "~r/https:\/\/example.com/u"]},
%{"tuple" => [":regex3", "~r/https:\/\/example.com/i"]},
%{"tuple" => [":regex4", "~r/https:\/\/example.com/s"]}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => "pleroma",
"key" => "Pleroma.Captcha.NotReal",
"value" => [
%{"tuple" => [":enabled", false]},
%{"tuple" => [":method", "Pleroma.Captcha.Kocaptcha"]},
%{"tuple" => [":seconds_valid", 60]},
%{"tuple" => [":path", ""]},
%{"tuple" => [":key1", nil]},
%{"tuple" => [":partial_chain", "&:hackney_connect.partial_chain/1"]},
%{"tuple" => [":regex1", "~r/https:\\/\\/example.com/"]},
%{"tuple" => [":regex2", "~r/https:\\/\\/example.com/u"]},
%{"tuple" => [":regex3", "~r/https:\\/\\/example.com/i"]},
%{"tuple" => [":regex4", "~r/https:\\/\\/example.com/s"]}
]
}
]
}
end
test "tuples with more than two values", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => "pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
"tuple" => [
":http",
[
%{
"tuple" => [
":key2",
[
%{
"tuple" => [
":_",
[
%{
"tuple" => [
"/api/v1/streaming",
"Pleroma.Web.MastodonAPI.WebsocketHandler",
[]
]
},
%{
"tuple" => [
"/websocket",
"Phoenix.Endpoint.CowboyWebSocket",
%{
"tuple" => [
"Phoenix.Transports.WebSocket",
%{
"tuple" => [
"Pleroma.Web.Endpoint",
"Pleroma.Web.UserSocket",
[]
]
}
]
}
]
},
%{
"tuple" => [
":_",
"Phoenix.Endpoint.Cowboy2Handler",
%{"tuple" => ["Pleroma.Web.Endpoint", []]}
]
}
]
]
}
]
]
}
]
]
}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => "pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
"tuple" => [
":http",
[
%{
"tuple" => [
":key2",
[
%{
"tuple" => [
":_",
[
%{
"tuple" => [
"/api/v1/streaming",
"Pleroma.Web.MastodonAPI.WebsocketHandler",
[]
]
},
%{
"tuple" => [
"/websocket",
"Phoenix.Endpoint.CowboyWebSocket",
%{
"tuple" => [
"Phoenix.Transports.WebSocket",
%{
"tuple" => [
"Pleroma.Web.Endpoint",
"Pleroma.Web.UserSocket",
[]
]
}
]
}
]
},
%{
"tuple" => [
":_",
"Phoenix.Endpoint.Cowboy2Handler",
%{"tuple" => ["Pleroma.Web.Endpoint", []]}
]
}
]
]
}
]
]
}
]
]
}
]
}
]
}
end
test "settings with nesting map", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => "pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key2", "some_val"]},
%{
"tuple" => [
":key3",
%{
":max_options" => 20,
":max_option_chars" => 200,
":min_expiration" => 0,
":max_expiration" => 31_536_000,
"nested" => %{
":max_options" => 20,
":max_option_chars" => 200,
":min_expiration" => 0,
":max_expiration" => 31_536_000
}
}
]
}
]
}
]
})
assert json_response(conn, 200) ==
%{
"configs" => [
%{
"group" => "pleroma",
"key" => ":key1",
"value" => [
%{"tuple" => [":key2", "some_val"]},
%{
"tuple" => [
":key3",
%{
":max_expiration" => 31_536_000,
":max_option_chars" => 200,
":max_options" => 20,
":min_expiration" => 0,
"nested" => %{
":max_expiration" => 31_536_000,
":max_option_chars" => 200,
":max_options" => 20,
":min_expiration" => 0
}
}
]
}
]
}
]
}
end
test "value as map", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => "pleroma",
"key" => ":key1",
"value" => %{"key" => "some_val"}
}
]
})
assert json_response(conn, 200) ==
%{
"configs" => [
%{
"group" => "pleroma",
"key" => ":key1",
"value" => %{"key" => "some_val"}
}
]
}
end
test "dispatch setting", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => "pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
"tuple" => [
":http",
[
%{"tuple" => [":ip", %{"tuple" => [127, 0, 0, 1]}]},
%{"tuple" => [":dispatch", ["{:_,
[
{\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{\"/websocket\", Phoenix.Endpoint.CowboyWebSocket,
{Phoenix.Transports.WebSocket,
{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}},
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
]}"]]}
]
]
}
]
}
]
})
dispatch_string =
"{:_, [{\"/api/v1/streaming\", Pleroma.Web.MastodonAPI.WebsocketHandler, []}, " <>
"{\"/websocket\", Phoenix.Endpoint.CowboyWebSocket, {Phoenix.Transports.WebSocket, " <>
"{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, [path: \"/websocket\"]}}}, " <>
"{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}]}"
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => "pleroma",
"key" => "Pleroma.Web.Endpoint.NotReal",
"value" => [
%{
"tuple" => [
":http",
[
%{"tuple" => [":ip", %{"tuple" => [127, 0, 0, 1]}]},
%{
"tuple" => [
":dispatch",
[
dispatch_string
]
]
}
]
]
}
]
}
]
}
end
test "queues key as atom", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
"group" => "oban",
"key" => ":queues",
"value" => [
%{"tuple" => [":federator_incoming", 50]},
%{"tuple" => [":federator_outgoing", 50]},
%{"tuple" => [":web_push", 50]},
%{"tuple" => [":mailer", 10]},
%{"tuple" => [":transmogrifier", 20]},
%{"tuple" => [":scheduled_activities", 10]},
%{"tuple" => [":background", 5]}
]
}
]
})
assert json_response(conn, 200) == %{
"configs" => [
%{
"group" => "oban",
"key" => ":queues",
"value" => [
%{"tuple" => [":federator_incoming", 50]},
%{"tuple" => [":federator_outgoing", 50]},
%{"tuple" => [":web_push", 50]},
%{"tuple" => [":mailer", 10]},
%{"tuple" => [":transmogrifier", 20]},
%{"tuple" => [":scheduled_activities", 10]},
%{"tuple" => [":background", 5]}
]
}
]
}
end
test "delete part of settings by atom subkeys", %{conn: conn} do
config =
insert(:config,
key: "keyaa1",
value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3")
)
conn =
post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
group: config.group,
key: config.key,
subkeys: [":subkey1", ":subkey3"],
delete: "true"
}
]
})
assert(
json_response(conn, 200) == %{
"configs" => [
%{
"group" => "pleroma",
"key" => "keyaa1",
"value" => [%{"tuple" => [":subkey2", "val2"]}]
}
]
}
)
end
end
describe "config mix tasks run" do
setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
temp_file = "config/test.exported_from_db.secret.exs"
Mix.shell(Mix.Shell.Quiet)
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
:ok = File.rm(temp_file)
end)
%{conn: assign(conn, :user, admin), admin: admin}
end
clear_config([:instance, :dynamic_configuration]) do
Pleroma.Config.put([:instance, :dynamic_configuration], true)
end
test "transfer settings to DB and to file", %{conn: conn, admin: admin} do
assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
conn = get(conn, "/api/pleroma/admin/config/migrate_to_db")
assert json_response(conn, 200) == %{}
assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) > 0
conn =
build_conn()
|> assign(:user, admin)
|> get("/api/pleroma/admin/config/migrate_from_db")
assert json_response(conn, 200) == %{}
assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
end
end
describe "GET /api/pleroma/admin/users/:nickname/statuses" do
setup do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = insert(:user)
date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!()
date2 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!()
date3 = (DateTime.to_unix(DateTime.utc_now()) + 3000) |> DateTime.from_unix!()
insert(:note_activity, user: user, published: date1)
insert(:note_activity, user: user, published: date2)
insert(:note_activity, user: user, published: date3)
conn =
build_conn()
|> assign(:user, admin)
{:ok, conn: conn, user: user}
end
test "renders user's statuses", %{conn: conn, user: user} do
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
assert json_response(conn, 200) |> length() == 3
end
test "renders user's statuses with a limit", %{conn: conn, user: user} do
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?page_size=2")
assert json_response(conn, 200) |> length() == 2
end
test "doesn't return private statuses by default", %{conn: conn, user: user} do
{:ok, _private_status} =
CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
{:ok, _public_status} =
CommonAPI.post(user, %{"status" => "public", "visibility" => "public"})
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses")
assert json_response(conn, 200) |> length() == 4
end
test "returns private statuses with godmode on", %{conn: conn, user: user} do
{:ok, _private_status} =
CommonAPI.post(user, %{"status" => "private", "visibility" => "private"})
{:ok, _public_status} =
CommonAPI.post(user, %{"status" => "public", "visibility" => "public"})
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses?godmode=true")
assert json_response(conn, 200) |> length() == 5
end
end
describe "GET /api/pleroma/admin/moderation_log" do
setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
- moderator = insert(:user, info: %{is_moderator: true})
+ admin = insert(:user, is_admin: true)
+ moderator = insert(:user, is_moderator: true)
%{conn: assign(conn, :user, admin), admin: admin, moderator: moderator}
end
test "returns the log", %{conn: conn, admin: admin} do
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second)
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second)
})
conn = get(conn, "/api/pleroma/admin/moderation_log")
response = json_response(conn, 200)
[first_entry, second_entry] = response["items"]
assert response["total"] == 2
assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] ==
"@#{admin.nickname} unfollowed relay: https://example.org/relay"
assert second_entry["data"]["action"] == "relay_follow"
assert second_entry["message"] ==
"@#{admin.nickname} followed relay: https://example.org/relay"
end
test "returns the log with pagination", %{conn: conn, admin: admin} do
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-15 15:47:06.597036], :second)
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.truncate(~N[2017-08-16 15:47:06.597036], :second)
})
conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1")
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 2
assert response1["items"] |> length() == 1
assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] ==
"@#{admin.nickname} unfollowed relay: https://example.org/relay"
conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2")
response2 = json_response(conn2, 200)
[second_entry] = response2["items"]
assert response2["total"] == 2
assert response2["items"] |> length() == 1
assert second_entry["data"]["action"] == "relay_follow"
assert second_entry["message"] ==
"@#{admin.nickname} followed relay: https://example.org/relay"
end
test "filters log by date", %{conn: conn, admin: admin} do
first_date = "2017-08-15T15:47:06Z"
second_date = "2017-08-20T15:47:06Z"
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.from_iso8601!(first_date)
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
},
inserted_at: NaiveDateTime.from_iso8601!(second_date)
})
conn1 =
get(
conn,
"/api/pleroma/admin/moderation_log?start_date=#{second_date}"
)
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 1
assert first_entry["data"]["action"] == "relay_unfollow"
assert first_entry["message"] ==
"@#{admin.nickname} unfollowed relay: https://example.org/relay"
end
test "returns log filtered by user", %{conn: conn, admin: admin, moderator: moderator} do
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => admin.id,
"nickname" => admin.nickname,
"type" => "user"
},
action: "relay_follow",
target: "https://example.org/relay"
}
})
Repo.insert(%ModerationLog{
data: %{
actor: %{
"id" => moderator.id,
"nickname" => moderator.nickname,
"type" => "user"
},
action: "relay_unfollow",
target: "https://example.org/relay"
}
})
conn1 = get(conn, "/api/pleroma/admin/moderation_log?user_id=#{moderator.id}")
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 1
assert get_in(first_entry, ["data", "actor", "id"]) == moderator.id
end
test "returns log filtered by search", %{conn: conn, moderator: moderator} do
ModerationLog.insert_log(%{
actor: moderator,
action: "relay_follow",
target: "https://example.org/relay"
})
ModerationLog.insert_log(%{
actor: moderator,
action: "relay_unfollow",
target: "https://example.org/relay"
})
conn1 = get(conn, "/api/pleroma/admin/moderation_log?search=unfo")
response1 = json_response(conn1, 200)
[first_entry] = response1["items"]
assert response1["total"] == 1
assert get_in(first_entry, ["data", "message"]) ==
"@#{moderator.nickname} unfollowed relay: https://example.org/relay"
end
end
describe "PATCH /users/:nickname/force_password_reset" do
setup %{conn: conn} do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
user = insert(:user)
%{conn: assign(conn, :user, admin), admin: admin, user: user}
end
test "sets password_reset_pending to true", %{admin: admin, user: user} do
- assert user.info.password_reset_pending == false
+ assert user.password_reset_pending == false
conn =
build_conn()
|> assign(:user, admin)
|> patch("/api/pleroma/admin/users/#{user.nickname}/force_password_reset")
assert json_response(conn, 204) == ""
ObanHelpers.perform_all()
- assert User.get_by_id(user.id).info.password_reset_pending == true
+ assert User.get_by_id(user.id).password_reset_pending == true
+ end
+ end
+
+ describe "relays" do
+ setup %{conn: conn} do
+ admin = insert(:user, is_admin: true)
+
+ %{conn: assign(conn, :user, admin), admin: admin}
+ end
+
+ test "POST /relay", %{admin: admin} do
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> post("/api/pleroma/admin/relay", %{
+ relay_url: "http://mastodon.example.org/users/admin"
+ })
+
+ assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
+
+ log_entry = Repo.one(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry) ==
+ "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
+ end
+
+ test "GET /relay", %{admin: admin} do
+ Pleroma.Web.ActivityPub.Relay.get_actor()
+ |> Ecto.Changeset.change(
+ following: [
+ "http://test-app.com/user/test1",
+ "http://test-app.com/user/test1",
+ "http://test-app-42.com/user/test1"
+ ]
+ )
+ |> Pleroma.User.update_and_set_cache()
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> get("/api/pleroma/admin/relay")
+
+ assert json_response(conn, 200)["relays"] -- ["test-app.com", "test-app-42.com"] == []
+ end
+
+ test "DELETE /relay", %{admin: admin} do
+ build_conn()
+ |> assign(:user, admin)
+ |> post("/api/pleroma/admin/relay", %{
+ relay_url: "http://mastodon.example.org/users/admin"
+ })
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> delete("/api/pleroma/admin/relay", %{
+ relay_url: "http://mastodon.example.org/users/admin"
+ })
+
+ assert json_response(conn, 200) == "http://mastodon.example.org/users/admin"
+
+ [log_entry_one, log_entry_two] = Repo.all(ModerationLog)
+
+ assert ModerationLog.get_log_entry_message(log_entry_one) ==
+ "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin"
+
+ assert ModerationLog.get_log_entry_message(log_entry_two) ==
+ "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin"
end
end
end
# Needed for testing
defmodule Pleroma.Web.Endpoint.NotReal do
end
defmodule Pleroma.Captcha.NotReal do
end
diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs
index 9df4cd539..082e691c4 100644
--- a/test/web/admin_api/search_test.exs
+++ b/test/web/admin_api/search_test.exs
@@ -1,170 +1,170 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.SearchTest do
use Pleroma.Web.ConnCase
alias Pleroma.Web.AdminAPI.Search
import Pleroma.Factory
describe "search for admin" do
test "it ignores case" do
insert(:user, nickname: "papercoach")
insert(:user, nickname: "CanadaPaperCoach")
{:ok, _results, count} =
Search.user(%{
query: "paper",
local: false,
page: 1,
page_size: 50
})
assert count == 2
end
test "it returns local/external users" do
insert(:user, local: true)
insert(:user, local: false)
insert(:user, local: false)
{:ok, _results, local_count} =
Search.user(%{
query: "",
local: true
})
{:ok, _results, external_count} =
Search.user(%{
query: "",
external: true
})
assert local_count == 1
assert external_count == 2
end
test "it returns active/deactivated users" do
- insert(:user, info: %{deactivated: true})
- insert(:user, info: %{deactivated: true})
- insert(:user, info: %{deactivated: false})
+ insert(:user, deactivated: true)
+ insert(:user, deactivated: true)
+ insert(:user, deactivated: false)
{:ok, _results, active_count} =
Search.user(%{
query: "",
active: true
})
{:ok, _results, deactivated_count} =
Search.user(%{
query: "",
deactivated: true
})
assert active_count == 1
assert deactivated_count == 2
end
test "it returns specific user" do
insert(:user)
insert(:user)
- user = insert(:user, nickname: "bob", local: true, info: %{deactivated: false})
+ user = insert(:user, nickname: "bob", local: true, deactivated: false)
{:ok, _results, total_count} = Search.user(%{query: ""})
{:ok, [^user], count} =
Search.user(%{
query: "Bo",
active: true,
local: true
})
assert total_count == 3
assert count == 1
end
test "it returns user by domain" do
insert(:user)
insert(:user)
user = insert(:user, nickname: "some@domain.com")
{:ok, _results, total} = Search.user()
{:ok, [^user], count} = Search.user(%{query: "domain.com"})
assert total == 3
assert count == 1
end
test "it return user by full nickname" do
insert(:user)
insert(:user)
user = insert(:user, nickname: "some@domain.com")
{:ok, _results, total} = Search.user()
{:ok, [^user], count} = Search.user(%{query: "some@domain.com"})
assert total == 3
assert count == 1
end
test "it returns admin user" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
insert(:user)
insert(:user)
{:ok, _results, total} = Search.user()
{:ok, [^admin], count} = Search.user(%{is_admin: true})
assert total == 3
assert count == 1
end
test "it returns moderator user" do
- moderator = insert(:user, info: %{is_moderator: true})
+ moderator = insert(:user, is_moderator: true)
insert(:user)
insert(:user)
{:ok, _results, total} = Search.user()
{:ok, [^moderator], count} = Search.user(%{is_moderator: true})
assert total == 3
assert count == 1
end
test "it returns users with tags" do
user1 = insert(:user, tags: ["first"])
user2 = insert(:user, tags: ["second"])
insert(:user)
insert(:user)
{:ok, _results, total} = Search.user()
{:ok, users, count} = Search.user(%{tags: ["first", "second"]})
assert total == 4
assert count == 2
assert user1 in users
assert user2 in users
end
test "it returns user by display name" do
user = insert(:user, name: "Display name")
insert(:user)
insert(:user)
{:ok, _results, total} = Search.user()
{:ok, [^user], count} = Search.user(%{name: "display"})
assert total == 3
assert count == 1
end
test "it returns user by email" do
user = insert(:user, email: "some@example.com")
insert(:user)
insert(:user)
{:ok, _results, total} = Search.user()
{:ok, [^user], count} = Search.user(%{email: "some@example.com"})
assert total == 3
assert count == 1
end
end
end
diff --git a/test/web/admin_api/views/report_view_test.exs b/test/web/admin_api/views/report_view_test.exs
index 475705857..ef4a806e4 100644
--- a/test/web/admin_api/views/report_view_test.exs
+++ b/test/web/admin_api/views/report_view_test.exs
@@ -1,133 +1,135 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ReportViewTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
test "renders a report" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.report(user, %{"account_id" => other_user.id})
expected = %{
content: nil,
actor:
Map.merge(
AccountView.render("show.json", %{user: user}),
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})
),
account:
Map.merge(
AccountView.render("show.json", %{user: other_user}),
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user})
),
statuses: [],
state: "open",
id: activity.id
}
result =
ReportView.render("show.json", Report.extract_report_info(activity))
|> Map.delete(:created_at)
assert result == expected
end
test "includes reported statuses" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "toot"})
{:ok, report_activity} =
CommonAPI.report(user, %{"account_id" => other_user.id, "status_ids" => [activity.id]})
+ other_user = Pleroma.User.get_by_id(other_user.id)
+
expected = %{
content: nil,
actor:
Map.merge(
AccountView.render("show.json", %{user: user}),
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})
),
account:
Map.merge(
AccountView.render("show.json", %{user: other_user}),
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user})
),
statuses: [StatusView.render("show.json", %{activity: activity})],
state: "open",
id: report_activity.id
}
result =
ReportView.render("show.json", Report.extract_report_info(report_activity))
|> Map.delete(:created_at)
assert result == expected
end
test "renders report's state" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.report(user, %{"account_id" => other_user.id})
{:ok, activity} = CommonAPI.update_report_state(activity.id, "closed")
assert %{state: "closed"} =
ReportView.render("show.json", Report.extract_report_info(activity))
end
test "renders report description" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.report(user, %{
"account_id" => other_user.id,
"comment" => "posts are too good for this instance"
})
assert %{content: "posts are too good for this instance"} =
ReportView.render("show.json", Report.extract_report_info(activity))
end
test "sanitizes report description" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.report(user, %{
"account_id" => other_user.id,
"comment" => ""
})
data = Map.put(activity.data, "content", "<script> alert('hecked :D:D:D:D:D:D:D') </script>")
activity = Map.put(activity, :data, data)
refute "<script> alert('hecked :D:D:D:D:D:D:D') </script>" ==
ReportView.render("show.json", Report.extract_report_info(activity))[:content]
end
test "doesn't error out when the user doesn't exists" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.report(user, %{
"account_id" => other_user.id,
"comment" => ""
})
Pleroma.User.delete(other_user)
Pleroma.User.invalidate_cache(other_user)
assert %{} = ReportView.render("show.json", Report.extract_report_info(activity))
end
end
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index d69ed38a7..f88fa5ed6 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -1,582 +1,582 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPITest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
require Pleroma.Constants
clear_config([:instance, :safe_dm_mentions])
clear_config([:instance, :limit])
clear_config([:instance, :max_pinned_statuses])
test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
[participation] = Participation.for_user(user)
{:ok, convo_reply} =
CommonAPI.post(user, %{"status" => ".", "in_reply_to_conversation_id" => participation.id})
assert Visibility.is_direct?(convo_reply)
assert activity.data["context"] == convo_reply.data["context"]
end
test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do
har = insert(:user)
jafnhar = insert(:user)
tridi = insert(:user)
{:ok, activity} =
CommonAPI.post(har, %{
"status" => "@#{jafnhar.nickname} hey",
"visibility" => "direct"
})
assert har.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
[participation] = Participation.for_user(har)
{:ok, activity} =
CommonAPI.post(har, %{
"status" => "I don't really like @#{tridi.nickname}",
"visibility" => "direct",
"in_reply_to_status_id" => activity.id,
"in_reply_to_conversation_id" => participation.id
})
assert har.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
refute tridi.ap_id in activity.recipients
end
test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do
har = insert(:user)
jafnhar = insert(:user)
tridi = insert(:user)
Pleroma.Config.put([:instance, :safe_dm_mentions], true)
{:ok, activity} =
CommonAPI.post(har, %{
"status" => "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again",
"visibility" => "direct"
})
refute tridi.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
end
test "it de-duplicates tags" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu #2HU"})
object = Object.normalize(activity)
assert object.data["tag"] == ["2hu"]
end
test "it adds emoji in the object" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ":firefox:"})
assert Object.normalize(activity).data["emoji"]["firefox"]
end
test "it adds emoji when updating profiles" do
user = insert(:user, %{name: ":firefox:"})
{:ok, activity} = CommonAPI.update(user)
user = User.get_cached_by_ap_id(user.ap_id)
- [firefox] = user.info.source_data["tag"]
+ [firefox] = user.source_data["tag"]
assert firefox["name"] == ":firefox:"
assert Pleroma.Constants.as_public() in activity.recipients
end
describe "posting" do
test "it supports explicit addressing" do
user = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
user_four = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" =>
"Hey, I think @#{user_three.nickname} is ugly. @#{user_four.nickname} is alright though.",
"to" => [user_two.nickname, user_four.nickname, "nonexistent"]
})
assert user.ap_id in activity.recipients
assert user_two.ap_id in activity.recipients
assert user_four.ap_id in activity.recipients
refute user_three.ap_id in activity.recipients
end
test "it filters out obviously bad tags when accepting a post as HTML" do
user = insert(:user)
post = "<p><b>2hu</b></p><script>alert('xss')</script>"
{:ok, activity} =
CommonAPI.post(user, %{
"status" => post,
"content_type" => "text/html"
})
object = Object.normalize(activity)
assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')"
end
test "it filters out obviously bad tags when accepting a post as Markdown" do
user = insert(:user)
post = "<p><b>2hu</b></p><script>alert('xss')</script>"
{:ok, activity} =
CommonAPI.post(user, %{
"status" => post,
"content_type" => "text/markdown"
})
object = Object.normalize(activity)
assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')"
end
test "it does not allow replies to direct messages that are not direct messages themselves" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"})
assert {:ok, _} =
CommonAPI.post(user, %{
"status" => "suya..",
"visibility" => "direct",
"in_reply_to_status_id" => activity.id
})
Enum.each(["public", "private", "unlisted"], fn visibility ->
assert {:error, "The message visibility must be direct"} =
CommonAPI.post(user, %{
"status" => "suya..",
"visibility" => visibility,
"in_reply_to_status_id" => activity.id
})
end)
end
test "it allows to address a list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
assert activity.data["bcc"] == [list.ap_id]
assert activity.recipients == [list.ap_id, user.ap_id]
assert activity.data["listMessage"] == list.ap_id
end
test "it returns error when status is empty and no attachments" do
user = insert(:user)
assert {:error, "Cannot post an empty status without attachments"} =
CommonAPI.post(user, %{"status" => ""})
end
test "it returns error when character limit is exceeded" do
Pleroma.Config.put([:instance, :limit], 5)
user = insert(:user)
assert {:error, "The status is over the character limit"} =
CommonAPI.post(user, %{"status" => "foobar"})
end
test "it can handle activities that expire" do
user = insert(:user)
expires_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.truncate(:second)
|> NaiveDateTime.add(1_000_000, :second)
assert {:ok, activity} =
CommonAPI.post(user, %{"status" => "chai", "expires_in" => 1_000_000})
assert expiration = Pleroma.ActivityExpiration.get_by_activity_id(activity.id)
assert expiration.scheduled_at == expires_at
end
end
describe "reactions" do
test "repeating a status" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user)
end
test "repeating a status privately" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, %Activity{} = announce_activity, _} =
CommonAPI.repeat(activity.id, user, %{"visibility" => "private"})
assert Visibility.is_private?(announce_activity)
end
test "favoriting a status" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, %Activity{}, _} = CommonAPI.favorite(activity.id, user)
end
test "retweeting a status twice returns an error" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, %Activity{}, _object} = CommonAPI.repeat(activity.id, user)
{:error, _} = CommonAPI.repeat(activity.id, user)
end
test "favoriting a status twice returns an error" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, %Activity{}, _object} = CommonAPI.favorite(activity.id, user)
{:error, _} = CommonAPI.favorite(activity.id, user)
end
end
describe "pinned statuses" do
setup do
Pleroma.Config.put([:instance, :max_pinned_statuses], 1)
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
[user: user, activity: activity]
end
test "pin status", %{user: user, activity: activity} do
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
id = activity.id
user = refresh_record(user)
- assert %User{info: %{pinned_activities: [^id]}} = user
+ assert %User{pinned_activities: [^id]} = user
end
test "unlisted statuses can be pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!", "visibility" => "unlisted"})
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
end
test "only self-authored can be pinned", %{activity: activity} do
user = insert(:user)
assert {:error, "Could not pin"} = CommonAPI.pin(activity.id, user)
end
test "max pinned statuses", %{user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user)
user = refresh_record(user)
assert {:error, "You have already pinned the maximum number of statuses"} =
CommonAPI.pin(activity_two.id, user)
end
test "unpin status", %{user: user, activity: activity} do
{:ok, activity} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
assert {:ok, ^activity} = CommonAPI.unpin(activity.id, user)
user = refresh_record(user)
- assert %User{info: %{pinned_activities: []}} = user
+ assert %User{pinned_activities: []} = user
end
test "should unpin when deleting a status", %{user: user, activity: activity} do
{:ok, activity} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
assert {:ok, _} = CommonAPI.delete(activity.id, user)
user = refresh_record(user)
- assert %User{info: %{pinned_activities: []}} = user
+ assert %User{pinned_activities: []} = user
end
end
describe "mute tests" do
setup do
user = insert(:user)
activity = insert(:note_activity)
[user: user, activity: activity]
end
test "add mute", %{user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
assert CommonAPI.thread_muted?(user, activity)
end
test "remove mute", %{user: user, activity: activity} do
CommonAPI.add_mute(user, activity)
{:ok, _} = CommonAPI.remove_mute(user, activity)
refute CommonAPI.thread_muted?(user, activity)
end
test "check that mutes can't be duplicate", %{user: user, activity: activity} do
CommonAPI.add_mute(user, activity)
{:error, _} = CommonAPI.add_mute(user, activity)
end
end
describe "reports" do
test "creates a report" do
reporter = insert(:user)
target_user = insert(:user)
{:ok, activity} = CommonAPI.post(target_user, %{"status" => "foobar"})
reporter_ap_id = reporter.ap_id
target_ap_id = target_user.ap_id
activity_ap_id = activity.data["id"]
comment = "foobar"
report_data = %{
"account_id" => target_user.id,
"comment" => comment,
"status_ids" => [activity.id]
}
note_obj = %{
"type" => "Note",
"id" => activity_ap_id,
"content" => "foobar",
"published" => activity.object.data["published"],
"actor" => AccountView.render("show.json", %{user: target_user})
}
assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"content" => ^comment,
"object" => [^target_ap_id, ^note_obj],
"state" => "open"
}
} = flag_activity
end
test "updates report state" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
assert report.data["state"] == "resolved"
[reported_user, activity_id] = report.data["object"]
assert reported_user == target_user.ap_id
assert activity_id == activity.data["id"]
end
test "does not update report state when state is unsupported" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
end
end
describe "reblog muting" do
setup do
muter = insert(:user)
muted = insert(:user)
[muter: muter, muted: muted]
end
test "add a reblog mute", %{muter: muter, muted: muted} do
{:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
assert User.showing_reblogs?(muter, muted) == false
end
test "remove a reblog mute", %{muter: muter, muted: muted} do
{:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
{:ok, muter} = CommonAPI.show_reblogs(muter, muted)
assert User.showing_reblogs?(muter, muted) == true
end
end
describe "unfollow/2" do
test "also unsubscribes a user" do
[follower, followed] = insert_pair(:user)
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
{:ok, followed} = User.subscribe(follower, followed)
assert User.subscribed_to?(follower, followed)
{:ok, follower} = CommonAPI.unfollow(follower, followed)
refute User.subscribed_to?(follower, followed)
end
end
describe "accept_follow_request/2" do
test "after acceptance, it sets all existing pending follow request states to 'accept'" do
- user = insert(:user, info: %{locked: true})
+ user = insert(:user, locked: true)
follower = insert(:user)
follower_two = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, user)
{:ok, follow_activity_two} = ActivityPub.follow(follower, user)
{:ok, follow_activity_three} = ActivityPub.follow(follower_two, user)
assert follow_activity.data["state"] == "pending"
assert follow_activity_two.data["state"] == "pending"
assert follow_activity_three.data["state"] == "pending"
{:ok, _follower} = CommonAPI.accept_follow_request(follower, user)
assert Repo.get(Activity, follow_activity.id).data["state"] == "accept"
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept"
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
end
test "after rejection, it sets all existing pending follow request states to 'reject'" do
- user = insert(:user, info: %{locked: true})
+ user = insert(:user, locked: true)
follower = insert(:user)
follower_two = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, user)
{:ok, follow_activity_two} = ActivityPub.follow(follower, user)
{:ok, follow_activity_three} = ActivityPub.follow(follower_two, user)
assert follow_activity.data["state"] == "pending"
assert follow_activity_two.data["state"] == "pending"
assert follow_activity_three.data["state"] == "pending"
{:ok, _follower} = CommonAPI.reject_follow_request(follower, user)
assert Repo.get(Activity, follow_activity.id).data["state"] == "reject"
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
end
end
describe "vote/3" do
test "does not allow to vote twice" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "Am I cute?",
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
})
object = Object.normalize(activity)
{:ok, _, object} = CommonAPI.vote(other_user, object, [0])
assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1])
end
end
describe "listen/2" do
test "returns a valid activity" do
user = insert(:user)
{:ok, activity} =
CommonAPI.listen(user, %{
"title" => "lain radio episode 1",
"album" => "lain radio",
"artist" => "lain",
"length" => 180_000
})
object = Object.normalize(activity)
assert object.data["title"] == "lain radio episode 1"
assert Visibility.get_visibility(activity) == "public"
end
test "respects visibility=private" do
user = insert(:user)
{:ok, activity} =
CommonAPI.listen(user, %{
"title" => "lain radio episode 1",
"album" => "lain radio",
"artist" => "lain",
"length" => 180_000,
"visibility" => "private"
})
object = Object.normalize(activity)
assert object.data["title"] == "lain radio episode 1"
assert Visibility.get_visibility(activity) == "private"
end
end
end
diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs
index 43a715706..c224197c3 100644
--- a/test/web/federator_test.exs
+++ b/test/web/federator_test.exs
@@ -1,258 +1,173 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.FederatorTest do
alias Pleroma.Instances
alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Federator
alias Pleroma.Workers.PublisherWorker
use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
import Mock
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
clear_config_all([:instance, :federating]) do
Pleroma.Config.put([:instance, :federating], true)
end
clear_config([:instance, :allow_relay])
clear_config([:instance, :rewrite_policy])
clear_config([:mrf_keyword])
describe "Publish an activity" do
setup do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI"})
relay_mock = {
Pleroma.Web.ActivityPub.Relay,
[],
[publish: fn _activity -> send(self(), :relay_publish) end]
}
%{activity: activity, relay_mock: relay_mock}
end
test "with relays active, it publishes to the relay", %{
activity: activity,
relay_mock: relay_mock
} do
with_mocks([relay_mock]) do
Federator.publish(activity)
ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
end
assert_received :relay_publish
end
test "with relays deactivated, it does not publish to the relay", %{
activity: activity,
relay_mock: relay_mock
} do
Pleroma.Config.put([:instance, :allow_relay], false)
with_mocks([relay_mock]) do
Federator.publish(activity)
ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
end
refute_received :relay_publish
end
end
describe "Targets reachability filtering in `publish`" do
test "it federates only to reachable instances via AP" do
user = insert(:user)
{inbox1, inbox2} =
{"https://domain.com/users/nick1/inbox", "https://domain2.com/users/nick2/inbox"}
insert(:user, %{
local: false,
nickname: "nick1@domain.com",
ap_id: "https://domain.com/users/nick1",
- info: %{ap_enabled: true, source_data: %{"inbox" => inbox1}}
+ source_data: %{"inbox" => inbox1},
+ ap_enabled: true
})
insert(:user, %{
local: false,
nickname: "nick2@domain2.com",
ap_id: "https://domain2.com/users/nick2",
- info: %{ap_enabled: true, source_data: %{"inbox" => inbox2}}
+ source_data: %{"inbox" => inbox2},
+ ap_enabled: true
})
dt = NaiveDateTime.utc_now()
Instances.set_unreachable(inbox1, dt)
Instances.set_consistently_unreachable(URI.parse(inbox2).host)
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"})
expected_dt = NaiveDateTime.to_iso8601(dt)
ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
assert ObanHelpers.member?(
%{
"op" => "publish_one",
"params" => %{"inbox" => inbox1, "unreachable_since" => expected_dt}
},
all_enqueued(worker: PublisherWorker)
)
end
-
- test "it federates only to reachable instances via Websub" do
- user = insert(:user)
- websub_topic = Pleroma.Web.OStatus.feed_path(user)
-
- sub1 =
- insert(:websub_subscription, %{
- topic: websub_topic,
- state: "active",
- callback: "http://pleroma.soykaf.com/cb"
- })
-
- sub2 =
- insert(:websub_subscription, %{
- topic: websub_topic,
- state: "active",
- callback: "https://pleroma2.soykaf.com/cb"
- })
-
- dt = NaiveDateTime.utc_now()
- Instances.set_unreachable(sub2.callback, dt)
-
- Instances.set_consistently_unreachable(sub1.callback)
-
- {:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"})
-
- expected_callback = sub2.callback
- expected_dt = NaiveDateTime.to_iso8601(dt)
-
- ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
-
- assert ObanHelpers.member?(
- %{
- "op" => "publish_one",
- "params" => %{
- "callback" => expected_callback,
- "unreachable_since" => expected_dt
- }
- },
- all_enqueued(worker: PublisherWorker)
- )
- end
-
- test "it federates only to reachable instances via Salmon" do
- user = insert(:user)
-
- _remote_user1 =
- insert(:user, %{
- local: false,
- nickname: "nick1@domain.com",
- ap_id: "https://domain.com/users/nick1",
- info: %{salmon: "https://domain.com/salmon"}
- })
-
- remote_user2 =
- insert(:user, %{
- local: false,
- nickname: "nick2@domain2.com",
- ap_id: "https://domain2.com/users/nick2",
- info: %{salmon: "https://domain2.com/salmon"}
- })
-
- remote_user2_id = remote_user2.id
-
- dt = NaiveDateTime.utc_now()
- Instances.set_unreachable(remote_user2.ap_id, dt)
-
- Instances.set_consistently_unreachable("domain.com")
-
- {:ok, _activity} =
- CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"})
-
- expected_dt = NaiveDateTime.to_iso8601(dt)
-
- ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
-
- assert ObanHelpers.member?(
- %{
- "op" => "publish_one",
- "params" => %{
- "recipient_id" => remote_user2_id,
- "unreachable_since" => expected_dt
- }
- },
- all_enqueued(worker: PublisherWorker)
- )
- end
end
describe "Receive an activity" do
test "successfully processes incoming AP docs with correct origin" do
params = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => "http://mastodon.example.org/users/admin",
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/activities/1",
"object" => %{
"type" => "Note",
"content" => "hi world!",
"id" => "http://mastodon.example.org/users/admin/objects/1",
"attributedTo" => "http://mastodon.example.org/users/admin"
},
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
}
assert {:ok, job} = Federator.incoming_ap_doc(params)
assert {:ok, _activity} = ObanHelpers.perform(job)
end
test "rejects incoming AP docs with incorrect origin" do
params = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => "https://niu.moe/users/rye",
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/activities/1",
"object" => %{
"type" => "Note",
"content" => "hi world!",
"id" => "http://mastodon.example.org/users/admin/objects/1",
"attributedTo" => "http://mastodon.example.org/users/admin"
},
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
}
assert {:ok, job} = Federator.incoming_ap_doc(params)
assert :error = ObanHelpers.perform(job)
end
test "it does not crash if MRF rejects the post" do
Pleroma.Config.put([:mrf_keyword, :reject], ["lain"])
Pleroma.Config.put(
[:instance, :rewrite_policy],
Pleroma.Web.ActivityPub.MRF.KeywordPolicy
)
params =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
assert {:ok, job} = Federator.incoming_ap_doc(params)
assert :error = ObanHelpers.perform(job)
end
end
end
diff --git a/test/web/masto_fe_controller_test.exs b/test/web/masto_fe_controller_test.exs
index ab9dab352..b5dbd4a25 100644
--- a/test/web/masto_fe_controller_test.exs
+++ b/test/web/masto_fe_controller_test.exs
@@ -1,84 +1,84 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastoFEController do
use Pleroma.Web.ConnCase
alias Pleroma.Config
alias Pleroma.User
import Pleroma.Factory
clear_config([:instance, :public])
test "put settings", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> put("/api/web/settings", %{"data" => %{"programming" => "socks"}})
assert _result = json_response(conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
- assert user.info.settings == %{"programming" => "socks"}
+ assert user.settings == %{"programming" => "socks"}
end
describe "index/2 redirections" do
setup %{conn: conn} do
session_opts = [
store: :cookie,
key: "_test",
signing_salt: "cooldude"
]
conn =
conn
|> Plug.Session.call(Plug.Session.init(session_opts))
|> fetch_session()
test_path = "/web/statuses/test"
%{conn: conn, path: test_path}
end
test "redirects not logged-in users to the login page", %{conn: conn, path: path} do
conn = get(conn, path)
assert conn.status == 302
assert redirected_to(conn) == "/web/login"
end
test "redirects not logged-in users to the login page on private instances", %{
conn: conn,
path: path
} do
Config.put([:instance, :public], false)
conn = get(conn, path)
assert conn.status == 302
assert redirected_to(conn) == "/web/login"
end
test "does not redirect logged in users to the login page", %{conn: conn, path: path} do
token = insert(:oauth_token)
conn =
conn
|> assign(:user, token.user)
|> put_session(:oauth_token, token.token)
|> get(path)
assert conn.status == 200
end
test "saves referer path to session", %{conn: conn, path: path} do
conn = get(conn, path)
return_to = Plug.Conn.get_session(conn, :return_to)
assert return_to == path
end
end
end
diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
index 618031b40..519b56d6c 100644
--- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
+++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
@@ -1,429 +1,429 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
alias Pleroma.Repo
alias Pleroma.User
use Pleroma.Web.ConnCase
import Pleroma.Factory
clear_config([:instance, :max_account_fields])
describe "updating credentials" do
test "sets user settings in a generic way", %{conn: conn} do
user = insert(:user)
res_conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
"pleroma_settings_store" => %{
pleroma_fe: %{
theme: "bla"
}
}
})
assert user = json_response(res_conn, 200)
assert user["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}}
user = Repo.get(User, user["id"])
res_conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
"pleroma_settings_store" => %{
masto_fe: %{
theme: "bla"
}
}
})
assert user = json_response(res_conn, 200)
assert user["pleroma"]["settings_store"] ==
%{
"pleroma_fe" => %{"theme" => "bla"},
"masto_fe" => %{"theme" => "bla"}
}
user = Repo.get(User, user["id"])
res_conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
"pleroma_settings_store" => %{
masto_fe: %{
theme: "blub"
}
}
})
assert user = json_response(res_conn, 200)
assert user["pleroma"]["settings_store"] ==
%{
"pleroma_fe" => %{"theme" => "bla"},
"masto_fe" => %{"theme" => "blub"}
}
end
test "updates the user's bio", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
"note" => "I drink #cofe with @#{user2.nickname}"
})
assert user = json_response(conn, 200)
assert user["note"] ==
~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a data-user="#{
user2.id
}" class="u-url mention" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span>)
end
test "updates the user's locking status", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{locked: "true"})
assert user = json_response(conn, 200)
assert user["locked"] == true
end
test "updates the user's default scope", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{default_scope: "cofe"})
assert user = json_response(conn, 200)
assert user["source"]["privacy"] == "cofe"
end
test "updates the user's hide_followers status", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{hide_followers: "true"})
assert user = json_response(conn, 200)
assert user["pleroma"]["hide_followers"] == true
end
test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
hide_followers_count: "true",
hide_follows_count: "true"
})
assert user = json_response(conn, 200)
assert user["pleroma"]["hide_followers_count"] == true
assert user["pleroma"]["hide_follows_count"] == true
end
test "updates the user's skip_thread_containment option", %{conn: conn} do
user = insert(:user)
response =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"})
|> json_response(200)
assert response["pleroma"]["skip_thread_containment"] == true
- assert refresh_record(user).info.skip_thread_containment
+ assert refresh_record(user).skip_thread_containment
end
test "updates the user's hide_follows status", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{hide_follows: "true"})
assert user = json_response(conn, 200)
assert user["pleroma"]["hide_follows"] == true
end
test "updates the user's hide_favorites status", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{hide_favorites: "true"})
assert user = json_response(conn, 200)
assert user["pleroma"]["hide_favorites"] == true
end
test "updates the user's show_role status", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{show_role: "false"})
assert user = json_response(conn, 200)
assert user["source"]["pleroma"]["show_role"] == false
end
test "updates the user's no_rich_text status", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{no_rich_text: "true"})
assert user = json_response(conn, 200)
assert user["source"]["pleroma"]["no_rich_text"] == true
end
test "updates the user's name", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})
assert user = json_response(conn, 200)
assert user["display_name"] == "markorepairs"
end
test "updates the user's avatar", %{conn: conn} do
user = insert(:user)
new_avatar = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
assert user_response = json_response(conn, 200)
assert user_response["avatar"] != User.avatar_url(user)
end
test "updates the user's banner", %{conn: conn} do
user = insert(:user)
new_header = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"header" => new_header})
assert user_response = json_response(conn, 200)
assert user_response["header"] != User.banner_url(user)
end
test "updates the user's background", %{conn: conn} do
user = insert(:user)
new_header = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
"pleroma_background_image" => new_header
})
assert user_response = json_response(conn, 200)
assert user_response["pleroma"]["background_image"]
end
test "requires 'write:accounts' permission", %{conn: conn} do
token1 = insert(:oauth_token, scopes: ["read"])
token2 = insert(:oauth_token, scopes: ["write", "follow"])
for token <- [token1, token2] do
conn =
conn
|> put_req_header("authorization", "Bearer #{token.token}")
|> patch("/api/v1/accounts/update_credentials", %{})
if token == token1 do
assert %{"error" => "Insufficient permissions: write:accounts."} ==
json_response(conn, 403)
else
assert json_response(conn, 200)
end
end
end
test "updates profile emojos", %{conn: conn} do
user = insert(:user)
note = "*sips :blank:*"
name = "I am :firefox:"
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
"note" => note,
"display_name" => name
})
assert json_response(conn, 200)
conn =
conn
|> get("/api/v1/accounts/#{user.id}")
assert user = json_response(conn, 200)
assert user["note"] == note
assert user["display_name"] == name
assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user["emojis"]
end
test "update fields", %{conn: conn} do
user = insert(:user)
fields = [
%{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "<script>bar</script>"},
%{"name" => "link", "value" => "cofe.io"}
]
account =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(200)
assert account["fields"] == [
%{"name" => "foo", "value" => "bar"},
%{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
]
assert account["source"]["fields"] == [
%{
"name" => "<a href=\"http://google.com\">foo</a>",
"value" => "<script>bar</script>"
},
%{"name" => "link", "value" => "cofe.io"}
]
fields =
[
"fields_attributes[1][name]=link",
"fields_attributes[1][value]=cofe.io",
"fields_attributes[0][name]=<a href=\"http://google.com\">foo</a>",
"fields_attributes[0][value]=bar"
]
|> Enum.join("&")
account =
conn
|> put_req_header("content-type", "application/x-www-form-urlencoded")
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", fields)
|> json_response(200)
assert account["fields"] == [
%{"name" => "foo", "value" => "bar"},
%{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
]
assert account["source"]["fields"] == [
%{
"name" => "<a href=\"http://google.com\">foo</a>",
"value" => "bar"
},
%{"name" => "link", "value" => "cofe.io"}
]
name_limit = Pleroma.Config.get([:instance, :account_field_name_length])
value_limit = Pleroma.Config.get([:instance, :account_field_value_length])
long_value = Enum.map(0..value_limit, fn _ -> "x" end) |> Enum.join()
fields = [%{"name" => "<b>foo<b>", "value" => long_value}]
assert %{"error" => "Invalid request"} ==
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403)
long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join()
fields = [%{"name" => long_name, "value" => "bar"}]
assert %{"error" => "Invalid request"} ==
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403)
Pleroma.Config.put([:instance, :max_account_fields], 1)
fields = [
%{"name" => "<b>foo<b>", "value" => "<i>bar</i>"},
%{"name" => "link", "value" => "cofe.io"}
]
assert %{"error" => "Invalid request"} ==
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(403)
fields = [
%{"name" => "foo", "value" => ""},
%{"name" => "", "value" => "bar"}
]
account =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
|> json_response(200)
assert account["fields"] == [
%{"name" => "foo", "value" => ""}
]
end
end
end
diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs
index 745383757..00c83fd7b 100644
--- a/test/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/web/mastodon_api/controllers/account_controller_test.exs
@@ -1,896 +1,896 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth.Token
import Pleroma.Factory
describe "account fetching" do
test "works by id" do
user = insert(:user)
conn =
build_conn()
|> get("/api/v1/accounts/#{user.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(user.id)
conn =
build_conn()
|> get("/api/v1/accounts/-1")
assert %{"error" => "Can't find user"} = json_response(conn, 404)
end
test "works by nickname" do
user = insert(:user)
conn =
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
assert %{"id" => id} = json_response(conn, 200)
assert id == user.id
end
test "works by nickname for remote users" do
limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
Pleroma.Config.put([:instance, :limit_to_local_content], false)
user = insert(:user, nickname: "user@example.com", local: false)
conn =
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
assert %{"id" => id} = json_response(conn, 200)
assert id == user.id
end
test "respects limit_to_local_content == :all for remote user nicknames" do
limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
Pleroma.Config.put([:instance, :limit_to_local_content], :all)
user = insert(:user, nickname: "user@example.com", local: false)
conn =
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
assert json_response(conn, 404)
end
test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do
limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
user = insert(:user, nickname: "user@example.com", local: false)
reading_user = insert(:user)
conn =
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
assert json_response(conn, 404)
conn =
build_conn()
|> assign(:user, reading_user)
|> get("/api/v1/accounts/#{user.nickname}")
Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
assert %{"id" => id} = json_response(conn, 200)
assert id == user.id
end
test "accounts fetches correct account for nicknames beginning with numbers", %{conn: conn} do
# Need to set an old-style integer ID to reproduce the problem
# (these are no longer assigned to new accounts but were preserved
# for existing accounts during the migration to flakeIDs)
user_one = insert(:user, %{id: 1212})
user_two = insert(:user, %{nickname: "#{user_one.id}garbage"})
resp_one =
conn
|> get("/api/v1/accounts/#{user_one.id}")
resp_two =
conn
|> get("/api/v1/accounts/#{user_two.nickname}")
resp_three =
conn
|> get("/api/v1/accounts/#{user_two.id}")
acc_one = json_response(resp_one, 200)
acc_two = json_response(resp_two, 200)
acc_three = json_response(resp_three, 200)
refute acc_one == acc_two
assert acc_two == acc_three
end
end
describe "user timelines" do
test "gets a users statuses", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
{:ok, user_three} = User.follow(user_three, user_one)
{:ok, activity} = CommonAPI.post(user_one, %{"status" => "HI!!!"})
{:ok, direct_activity} =
CommonAPI.post(user_one, %{
"status" => "Hi, @#{user_two.nickname}.",
"visibility" => "direct"
})
{:ok, private_activity} =
CommonAPI.post(user_one, %{"status" => "private", "visibility" => "private"})
resp =
conn
|> get("/api/v1/accounts/#{user_one.id}/statuses")
assert [%{"id" => id}] = json_response(resp, 200)
assert id == to_string(activity.id)
resp =
conn
|> assign(:user, user_two)
|> get("/api/v1/accounts/#{user_one.id}/statuses")
assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
assert id_one == to_string(direct_activity.id)
assert id_two == to_string(activity.id)
resp =
conn
|> assign(:user, user_three)
|> get("/api/v1/accounts/#{user_one.id}/statuses")
assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
assert id_one == to_string(private_activity.id)
assert id_two == to_string(activity.id)
end
test "unimplemented pinned statuses feature", %{conn: conn} do
note = insert(:note_activity)
user = User.get_cached_by_ap_id(note.data["actor"])
conn =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
assert json_response(conn, 200) == []
end
test "gets an users media", %{conn: conn} do
note = insert(:note_activity)
user = User.get_cached_by_ap_id(note.data["actor"])
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id)
{:ok, image_post} = CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media_id]})
conn =
conn
|> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(image_post.id)
conn =
build_conn()
|> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(image_post.id)
end
test "gets a user's statuses without reblogs", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, _, _} = CommonAPI.repeat(post.id, user)
conn =
conn
|> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(post.id)
conn =
conn
|> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(post.id)
end
test "filters user's statuses by a hashtag", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{"status" => "#hashtag"})
{:ok, _post} = CommonAPI.post(user, %{"status" => "hashtag"})
conn =
conn
|> get("/api/v1/accounts/#{user.id}/statuses", %{"tagged" => "hashtag"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(post.id)
end
test "the user views their own timelines and excludes direct messages", %{conn: conn} do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, _direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_visibilities" => ["direct"]})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(public_activity.id)
end
end
describe "followers" do
test "getting followers", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
conn =
conn
|> get("/api/v1/accounts/#{other_user.id}/followers")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(user.id)
end
test "getting followers, hide_followers", %{conn: conn} do
user = insert(:user)
- other_user = insert(:user, %{info: %{hide_followers: true}})
+ other_user = insert(:user, hide_followers: true)
{:ok, _user} = User.follow(user, other_user)
conn =
conn
|> get("/api/v1/accounts/#{other_user.id}/followers")
assert [] == json_response(conn, 200)
end
test "getting followers, hide_followers, same user requesting", %{conn: conn} do
user = insert(:user)
- other_user = insert(:user, %{info: %{hide_followers: true}})
+ other_user = insert(:user, hide_followers: true)
{:ok, _user} = User.follow(user, other_user)
conn =
conn
|> assign(:user, other_user)
|> get("/api/v1/accounts/#{other_user.id}/followers")
refute [] == json_response(conn, 200)
end
test "getting followers, pagination", %{conn: conn} do
user = insert(:user)
follower1 = insert(:user)
follower2 = insert(:user)
follower3 = insert(:user)
{:ok, _} = User.follow(follower1, user)
{:ok, _} = User.follow(follower2, user)
{:ok, _} = User.follow(follower3, user)
conn =
conn
|> assign(:user, user)
res_conn =
conn
|> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1.id}")
assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
assert id3 == follower3.id
assert id2 == follower2.id
res_conn =
conn
|> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3.id}")
assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
assert id2 == follower2.id
assert id1 == follower1.id
res_conn =
conn
|> get("/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3.id}")
assert [%{"id" => id2}] = json_response(res_conn, 200)
assert id2 == follower2.id
assert [link_header] = get_resp_header(res_conn, "link")
assert link_header =~ ~r/min_id=#{follower2.id}/
assert link_header =~ ~r/max_id=#{follower2.id}/
end
end
describe "following" do
test "getting following", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
conn =
conn
|> get("/api/v1/accounts/#{user.id}/following")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(other_user.id)
end
test "getting following, hide_follows", %{conn: conn} do
- user = insert(:user, %{info: %{hide_follows: true}})
+ user = insert(:user, hide_follows: true)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
conn =
conn
|> get("/api/v1/accounts/#{user.id}/following")
assert [] == json_response(conn, 200)
end
test "getting following, hide_follows, same user requesting", %{conn: conn} do
- user = insert(:user, %{info: %{hide_follows: true}})
+ user = insert(:user, hide_follows: true)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/#{user.id}/following")
refute [] == json_response(conn, 200)
end
test "getting following, pagination", %{conn: conn} do
user = insert(:user)
following1 = insert(:user)
following2 = insert(:user)
following3 = insert(:user)
{:ok, _} = User.follow(user, following1)
{:ok, _} = User.follow(user, following2)
{:ok, _} = User.follow(user, following3)
conn =
conn
|> assign(:user, user)
res_conn =
conn
|> get("/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}")
assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200)
assert id3 == following3.id
assert id2 == following2.id
res_conn =
conn
|> get("/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}")
assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200)
assert id2 == following2.id
assert id1 == following1.id
res_conn =
conn
|> get("/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}")
assert [%{"id" => id2}] = json_response(res_conn, 200)
assert id2 == following2.id
assert [link_header] = get_resp_header(res_conn, "link")
assert link_header =~ ~r/min_id=#{following2.id}/
assert link_header =~ ~r/max_id=#{following2.id}/
end
end
describe "follow/unfollow" do
test "following / unfollowing a user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/follow")
assert %{"id" => _id, "following" => true} = json_response(conn, 200)
user = User.get_cached_by_id(user.id)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/unfollow")
assert %{"id" => _id, "following" => false} = json_response(conn, 200)
user = User.get_cached_by_id(user.id)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/follows", %{"uri" => other_user.nickname})
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(other_user.id)
end
test "following without reblogs" do
follower = insert(:user)
followed = insert(:user)
other_user = insert(:user)
conn =
build_conn()
|> assign(:user, follower)
|> post("/api/v1/accounts/#{followed.id}/follow?reblogs=false")
assert %{"showing_reblogs" => false} = json_response(conn, 200)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"})
{:ok, reblog, _} = CommonAPI.repeat(activity.id, followed)
conn =
build_conn()
|> assign(:user, User.get_cached_by_id(follower.id))
|> get("/api/v1/timelines/home")
assert [] == json_response(conn, 200)
conn =
build_conn()
|> assign(:user, follower)
|> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true")
assert %{"showing_reblogs" => true} = json_response(conn, 200)
conn =
build_conn()
|> assign(:user, User.get_cached_by_id(follower.id))
|> get("/api/v1/timelines/home")
expected_activity_id = reblog.id
assert [%{"id" => ^expected_activity_id}] = json_response(conn, 200)
end
test "following / unfollowing errors" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
# self follow
conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
# self unfollow
user = User.get_cached_by_id(user.id)
conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow")
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
# self follow via uri
user = User.get_cached_by_id(user.id)
conn_res = post(conn, "/api/v1/follows", %{"uri" => user.nickname})
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
# follow non existing user
conn_res = post(conn, "/api/v1/accounts/doesntexist/follow")
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
# follow non existing user via uri
conn_res = post(conn, "/api/v1/follows", %{"uri" => "doesntexist"})
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
# unfollow non existing user
conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow")
assert %{"error" => "Record not found"} = json_response(conn_res, 404)
end
end
describe "mute/unmute" do
test "with notifications", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/mute")
response = json_response(conn, 200)
assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = response
user = User.get_cached_by_id(user.id)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/unmute")
response = json_response(conn, 200)
assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
end
test "without notifications", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"})
response = json_response(conn, 200)
assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = response
user = User.get_cached_by_id(user.id)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/unmute")
response = json_response(conn, 200)
assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response
end
end
describe "pinned statuses" do
setup do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
[user: user, activity: activity]
end
test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.pin(activity.id, user)
result =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response(200)
id_str = to_string(activity.id)
assert [%{"id" => ^id_str, "pinned" => true}] = result
end
end
test "blocking / unblocking a user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/block")
assert %{"id" => _id, "blocking" => true} = json_response(conn, 200)
user = User.get_cached_by_id(user.id)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/unblock")
assert %{"id" => _id, "blocking" => false} = json_response(conn, 200)
end
describe "create account by app" do
setup do
valid_params = %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
agreement: true
}
[valid_params: valid_params]
end
test "Account registration via Application", %{conn: conn} do
conn =
conn
|> post("/api/v1/apps", %{
client_name: "client_name",
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
scopes: "read, write, follow"
})
%{
"client_id" => client_id,
"client_secret" => client_secret,
"id" => _,
"name" => "client_name",
"redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
"vapid_key" => _,
"website" => nil
} = json_response(conn, 200)
conn =
conn
|> post("/oauth/token", %{
grant_type: "client_credentials",
client_id: client_id,
client_secret: client_secret
})
assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
json_response(conn, 200)
assert token
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
assert refresh
assert scope == "read write follow"
conn =
build_conn()
|> put_req_header("authorization", "Bearer " <> token)
|> post("/api/v1/accounts", %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
bio: "Test Bio",
agreement: true
})
%{
"access_token" => token,
"created_at" => _created_at,
"scope" => _scope,
"token_type" => "Bearer"
} = json_response(conn, 200)
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
token_from_db = Repo.preload(token_from_db, :user)
assert token_from_db.user
- assert token_from_db.user.info.confirmation_pending
+ assert token_from_db.user.confirmation_pending
end
test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do
_user = insert(:user, email: "lain@example.org")
app_token = insert(:oauth_token, user: nil)
conn =
conn
|> put_req_header("authorization", "Bearer " <> app_token.token)
res = post(conn, "/api/v1/accounts", valid_params)
assert json_response(res, 400) == %{"error" => "{\"email\":[\"has already been taken\"]}"}
end
test "rate limit", %{conn: conn} do
app_token = insert(:oauth_token, user: nil)
conn =
put_req_header(conn, "authorization", "Bearer " <> app_token.token)
|> Map.put(:remote_ip, {15, 15, 15, 15})
for i <- 1..5 do
conn =
conn
|> post("/api/v1/accounts", %{
username: "#{i}lain",
email: "#{i}lain@example.org",
password: "PlzDontHackLain",
agreement: true
})
%{
"access_token" => token,
"created_at" => _created_at,
"scope" => _scope,
"token_type" => "Bearer"
} = json_response(conn, 200)
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
token_from_db = Repo.preload(token_from_db, :user)
assert token_from_db.user
- assert token_from_db.user.info.confirmation_pending
+ assert token_from_db.user.confirmation_pending
end
conn =
conn
|> post("/api/v1/accounts", %{
username: "6lain",
email: "6lain@example.org",
password: "PlzDontHackLain",
agreement: true
})
assert json_response(conn, :too_many_requests) == %{"error" => "Throttled"}
end
test "returns bad_request if missing required params", %{
conn: conn,
valid_params: valid_params
} do
app_token = insert(:oauth_token, user: nil)
conn =
conn
|> put_req_header("authorization", "Bearer " <> app_token.token)
res = post(conn, "/api/v1/accounts", valid_params)
assert json_response(res, 200)
[{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}]
|> Stream.zip(valid_params)
|> Enum.each(fn {ip, {attr, _}} ->
res =
conn
|> Map.put(:remote_ip, ip)
|> post("/api/v1/accounts", Map.delete(valid_params, attr))
|> json_response(400)
assert res == %{"error" => "Missing parameters"}
end)
end
test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do
conn =
conn
|> put_req_header("authorization", "Bearer " <> "invalid-token")
res = post(conn, "/api/v1/accounts", valid_params)
assert json_response(res, 403) == %{"error" => "Invalid credentials"}
end
end
describe "GET /api/v1/accounts/:id/lists - account_lists" do
test "returns lists to which the account belongs", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
assert {:ok, %Pleroma.List{} = list} = Pleroma.List.create("Test List", user)
{:ok, %{following: _following}} = Pleroma.List.follow(list, other_user)
res =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/#{other_user.id}/lists")
|> json_response(200)
assert res == [%{"id" => to_string(list.id), "title" => "Test List"}]
end
end
describe "verify_credentials" do
test "verify_credentials", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/verify_credentials")
response = json_response(conn, 200)
assert %{"id" => id, "source" => %{"privacy" => "public"}} = response
assert response["pleroma"]["chat_token"]
assert id == to_string(user.id)
end
test "verify_credentials default scope unlisted", %{conn: conn} do
- user = insert(:user, %{info: %User.Info{default_scope: "unlisted"}})
+ user = insert(:user, default_scope: "unlisted")
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/verify_credentials")
assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = json_response(conn, 200)
assert id == to_string(user.id)
end
test "locked accounts", %{conn: conn} do
- user = insert(:user, %{info: %User.Info{default_scope: "private"}})
+ user = insert(:user, default_scope: "private")
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/verify_credentials")
assert %{"id" => id, "source" => %{"privacy" => "private"}} = json_response(conn, 200)
assert id == to_string(user.id)
end
end
describe "user relationships" do
test "returns the relationships for the current user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/relationships", %{"id" => [other_user.id]})
assert [relationship] = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
end
test "returns an empty list on a bad request", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/relationships", %{})
assert [] = json_response(conn, 200)
end
end
test "getting a list of mutes", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.mute(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/mutes")
other_user_id = to_string(other_user.id)
assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
end
test "getting a list of blocks", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/blocks")
other_user_id = to_string(other_user.id)
assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
end
end
diff --git a/test/web/mastodon_api/controllers/conversation_controller_test.exs b/test/web/mastodon_api/controllers/conversation_controller_test.exs
index a308a7620..542af4944 100644
--- a/test/web/mastodon_api/controllers/conversation_controller_test.exs
+++ b/test/web/mastodon_api/controllers/conversation_controller_test.exs
@@ -1,156 +1,162 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
test "returns a list of conversations", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
{:ok, user_two} = User.follow(user_two, user_one)
- assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!",
"visibility" => "direct"
})
- assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 1
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1
{:ok, _follower_only} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "private"
})
res_conn =
conn
|> assign(:user, user_one)
|> get("/api/v1/conversations")
assert response = json_response(res_conn, 200)
assert [
%{
"id" => res_id,
"accounts" => res_accounts,
"last_status" => res_last_status,
"unread" => unread
}
] = response
account_ids = Enum.map(res_accounts, & &1["id"])
assert length(res_accounts) == 2
assert user_two.id in account_ids
assert user_three.id in account_ids
assert is_binary(res_id)
- assert unread == true
+ assert unread == false
assert res_last_status["id"] == direct.id
- assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
+ assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0
end
test "updates the last_status on reply", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}",
"visibility" => "direct"
})
{:ok, direct_reply} =
CommonAPI.post(user_two, %{
"status" => "reply",
"visibility" => "direct",
"in_reply_to_status_id" => direct.id
})
[%{"last_status" => res_last_status}] =
conn
|> assign(:user, user_one)
|> get("/api/v1/conversations")
|> json_response(200)
assert res_last_status["id"] == direct_reply.id
end
test "the user marks a conversation as read", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}",
"visibility" => "direct"
})
+ assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1
+
[%{"id" => direct_conversation_id, "unread" => true}] =
conn
- |> assign(:user, user_one)
+ |> assign(:user, user_two)
|> get("/api/v1/conversations")
|> json_response(200)
%{"unread" => false} =
conn
- |> assign(:user, user_one)
+ |> assign(:user, user_two)
|> post("/api/v1/conversations/#{direct_conversation_id}/read")
|> json_response(200)
- assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 0
+ assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
# The conversation is marked as unread on reply
{:ok, _} =
CommonAPI.post(user_two, %{
"status" => "reply",
"visibility" => "direct",
"in_reply_to_status_id" => direct.id
})
[%{"unread" => true}] =
conn
|> assign(:user, user_one)
|> get("/api/v1/conversations")
|> json_response(200)
- assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
+ assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
# A reply doesn't increment the user's unread_conversation_count if the conversation is unread
{:ok, _} =
CommonAPI.post(user_two, %{
"status" => "reply",
"visibility" => "direct",
"in_reply_to_status_id" => direct.id
})
- assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
+ assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1
+ assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0
end
test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "direct"
})
res_conn =
conn
|> assign(:user, user_one)
|> get("/api/v1/statuses/#{direct.id}/context")
assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)
end
end
diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs
index 4bf292df5..913f8c038 100644
--- a/test/web/mastodon_api/controllers/follow_request_controller_test.exs
+++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs
@@ -1,81 +1,81 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
import Pleroma.Factory
describe "locked accounts" do
test "/api/v1/follow_requests works" do
- user = insert(:user, %{info: %User.Info{locked: true}})
+ user = insert(:user, locked: true)
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> get("/api/v1/follow_requests")
assert [relationship] = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
end
test "/api/v1/follow_requests/:id/authorize works" do
- user = insert(:user, %{info: %User.Info{locked: true}})
+ user = insert(:user, locked: true)
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/follow_requests/#{other_user.id}/authorize")
assert relationship = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
assert User.following?(other_user, user) == true
end
test "/api/v1/follow_requests/:id/reject works" do
- user = insert(:user, %{info: %User.Info{locked: true}})
+ user = insert(:user, locked: true)
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
user = User.get_cached_by_id(user.id)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/follow_requests/#{other_user.id}/reject")
assert relationship = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
assert User.following?(other_user, user) == false
end
end
end
diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs
index f8049f81f..e00de6b18 100644
--- a/test/web/mastodon_api/controllers/instance_controller_test.exs
+++ b/test/web/mastodon_api/controllers/instance_controller_test.exs
@@ -1,84 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.User
import Pleroma.Factory
test "get instance information", %{conn: conn} do
conn = get(conn, "/api/v1/instance")
assert result = json_response(conn, 200)
email = Pleroma.Config.get([:instance, :email])
# Note: not checking for "max_toot_chars" since it's optional
assert %{
"uri" => _,
"title" => _,
"description" => _,
"version" => _,
"email" => from_config_email,
"urls" => %{
"streaming_api" => _
},
"stats" => _,
"thumbnail" => _,
"languages" => _,
"registrations" => _,
"poll_limits" => _,
"upload_limit" => _,
"avatar_upload_limit" => _,
"background_upload_limit" => _,
"banner_upload_limit" => _
} = result
assert email == from_config_email
end
test "get instance stats", %{conn: conn} do
user = insert(:user, %{local: true})
user2 = insert(:user, %{local: true})
- {:ok, _user2} = User.deactivate(user2, !user2.info.deactivated)
+ {:ok, _user2} = User.deactivate(user2, !user2.deactivated)
insert(:user, %{local: false, nickname: "u@peer1.com"})
insert(:user, %{local: false, nickname: "u@peer2.com"})
{:ok, _} = Pleroma.Web.CommonAPI.post(user, %{"status" => "cofe"})
- # Stats should count users with missing or nil `info.deactivated` value
-
- {:ok, _user} =
- user.id
- |> User.get_cached_by_id()
- |> User.update_info(&Ecto.Changeset.change(&1, %{deactivated: nil}))
-
Pleroma.Stats.force_update()
conn = get(conn, "/api/v1/instance")
assert result = json_response(conn, 200)
stats = result["stats"]
assert stats
assert stats["user_count"] == 1
assert stats["status_count"] == 1
assert stats["domain_count"] == 2
end
test "get peers", %{conn: conn} do
insert(:user, %{local: false, nickname: "u@peer1.com"})
insert(:user, %{local: false, nickname: "u@peer2.com"})
Pleroma.Stats.force_update()
conn = get(conn, "/api/v1/instance/peers")
assert result = json_response(conn, 200)
assert ["peer1.com", "peer2.com"] == Enum.sort(result)
end
end
diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs
new file mode 100644
index 000000000..1fcad873d
--- /dev/null
+++ b/test/web/mastodon_api/controllers/marker_controller_test.exs
@@ -0,0 +1,124 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
+ use Pleroma.Web.ConnCase
+
+ import Pleroma.Factory
+
+ describe "GET /api/v1/markers" do
+ test "gets markers with correct scopes", %{conn: conn} do
+ user = insert(:user)
+ token = insert(:oauth_token, user: user, scopes: ["read:statuses"])
+
+ {:ok, %{"notifications" => marker}} =
+ Pleroma.Marker.upsert(
+ user,
+ %{"notifications" => %{"last_read_id" => "69420"}}
+ )
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> get("/api/v1/markers", %{timeline: ["notifications"]})
+ |> json_response(200)
+
+ assert response == %{
+ "notifications" => %{
+ "last_read_id" => "69420",
+ "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
+ "version" => 0
+ }
+ }
+ end
+
+ test "gets markers with missed scopes", %{conn: conn} do
+ user = insert(:user)
+ token = insert(:oauth_token, user: user, scopes: [])
+
+ Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => "69420"}})
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> get("/api/v1/markers", %{timeline: ["notifications"]})
+ |> json_response(403)
+
+ assert response == %{"error" => "Insufficient permissions: read:statuses."}
+ end
+ end
+
+ describe "POST /api/v1/markers" do
+ test "creates a marker with correct scopes", %{conn: conn} do
+ user = insert(:user)
+ token = insert(:oauth_token, user: user, scopes: ["write:statuses"])
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> post("/api/v1/markers", %{
+ home: %{last_read_id: "777"},
+ notifications: %{"last_read_id" => "69420"}
+ })
+ |> json_response(200)
+
+ assert %{
+ "notifications" => %{
+ "last_read_id" => "69420",
+ "updated_at" => _,
+ "version" => 0
+ }
+ } = response
+ end
+
+ test "updates exist marker", %{conn: conn} do
+ user = insert(:user)
+ token = insert(:oauth_token, user: user, scopes: ["write:statuses"])
+
+ {:ok, %{"notifications" => marker}} =
+ Pleroma.Marker.upsert(
+ user,
+ %{"notifications" => %{"last_read_id" => "69477"}}
+ )
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> post("/api/v1/markers", %{
+ home: %{last_read_id: "777"},
+ notifications: %{"last_read_id" => "69888"}
+ })
+ |> json_response(200)
+
+ assert response == %{
+ "notifications" => %{
+ "last_read_id" => "69888",
+ "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
+ "version" => 0
+ }
+ }
+ end
+
+ test "creates a marker with missed scopes", %{conn: conn} do
+ user = insert(:user)
+ token = insert(:oauth_token, user: user, scopes: [])
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, token)
+ |> post("/api/v1/markers", %{
+ home: %{last_read_id: "777"},
+ notifications: %{"last_read_id" => "69420"}
+ })
+ |> json_response(403)
+
+ assert response == %{"error" => "Insufficient permissions: write:statuses."}
+ end
+ end
+end
diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs
index ee413eef7..7953fad62 100644
--- a/test/web/mastodon_api/controllers/search_controller_test.exs
+++ b/test/web/mastodon_api/controllers/search_controller_test.exs
@@ -1,292 +1,292 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Object
alias Pleroma.Web
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
import ExUnit.CaptureLog
import Tesla.Mock
import Mock
setup do
mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe ".search2" do
test "it returns empty result if user or status search return undefined error", %{conn: conn} do
with_mocks [
{Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]},
{Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]}
] do
capture_log(fn ->
results =
conn
|> get("/api/v2/search", %{"q" => "2hu"})
|> json_response(200)
assert results["accounts"] == []
assert results["statuses"] == []
end) =~
"[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}"
end
end
test "search", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu private 天子"})
{:ok, _activity} =
CommonAPI.post(user, %{
"status" => "This is about 2hu, but private",
"visibility" => "private"
})
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
results =
get(conn, "/api/v2/search", %{"q" => "2hu #private"})
|> json_response(200)
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
assert results["hashtags"] == [
%{"name" => "private", "url" => "#{Web.base_url()}/tag/private"}
]
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
results =
get(conn, "/api/v2/search", %{"q" => "天子"})
|> json_response(200)
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
end
end
describe ".account_search" do
test "account search", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
results =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "shp"})
|> json_response(200)
result_ids = for result <- results, do: result["acct"]
assert user_two.nickname in result_ids
assert user_three.nickname in result_ids
results =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "2hu"})
|> json_response(200)
result_ids = for result <- results, do: result["acct"]
assert user_three.nickname in result_ids
end
test "returns account if query contains a space", %{conn: conn} do
user = insert(:user, %{nickname: "shp@shitposter.club"})
results =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "})
|> json_response(200)
assert length(results) == 1
end
end
describe ".search" do
test "it returns empty result if user or status search return undefined error", %{conn: conn} do
with_mocks [
{Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]},
{Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]}
] do
capture_log(fn ->
results =
conn
|> get("/api/v1/search", %{"q" => "2hu"})
|> json_response(200)
assert results["accounts"] == []
assert results["statuses"] == []
end) =~
"[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}"
end
end
test "search", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"})
{:ok, _activity} =
CommonAPI.post(user, %{
"status" => "This is about 2hu, but private",
"visibility" => "private"
})
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
conn =
conn
|> get("/api/v1/search", %{"q" => "2hu"})
assert results = json_response(conn, 200)
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
assert results["hashtags"] == []
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
end
test "search fetches remote statuses", %{conn: conn} do
capture_log(fn ->
conn =
conn
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
assert results = json_response(conn, 200)
[status] = results["statuses"]
assert status["uri"] ==
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
end)
end
test "search doesn't show statuses that it shouldn't", %{conn: conn} do
{:ok, activity} =
CommonAPI.post(insert(:user), %{
"status" => "This is about 2hu, but private",
"visibility" => "private"
})
capture_log(fn ->
conn =
conn
|> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]})
assert results = json_response(conn, 200)
[] = results["statuses"]
end)
end
test "search fetches remote accounts", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
- |> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "true"})
+ |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"})
assert results = json_response(conn, 200)
[account] = results["accounts"]
- assert account["acct"] == "shp@social.heldscal.la"
+ assert account["acct"] == "mike@osada.macgirvin.com"
end
test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do
conn =
conn
- |> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "false"})
+ |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "false"})
assert results = json_response(conn, 200)
assert [] == results["accounts"]
end
test "search with limit and offset", %{conn: conn} do
user = insert(:user)
_user_two = insert(:user, %{nickname: "shp@shitposter.club"})
_user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
{:ok, _activity1} = CommonAPI.post(user, %{"status" => "This is about 2hu"})
{:ok, _activity2} = CommonAPI.post(user, %{"status" => "This is also about 2hu"})
result =
conn
|> get("/api/v1/search", %{"q" => "2hu", "limit" => 1})
assert results = json_response(result, 200)
assert [%{"id" => activity_id1}] = results["statuses"]
assert [_] = results["accounts"]
results =
conn
|> get("/api/v1/search", %{"q" => "2hu", "limit" => 1, "offset" => 1})
|> json_response(200)
assert [%{"id" => activity_id2}] = results["statuses"]
assert [] = results["accounts"]
assert activity_id1 != activity_id2
end
test "search returns results only for the given type", %{conn: conn} do
user = insert(:user)
_user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
{:ok, _activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"})
assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} =
conn
|> get("/api/v1/search", %{"q" => "2hu", "type" => "statuses"})
|> json_response(200)
assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} =
conn
|> get("/api/v1/search", %{"q" => "2hu", "type" => "accounts"})
|> json_response(200)
end
test "search uses account_id to filter statuses by the author", %{conn: conn} do
user = insert(:user, %{nickname: "shp@shitposter.club"})
user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
{:ok, activity1} = CommonAPI.post(user, %{"status" => "This is about 2hu"})
{:ok, activity2} = CommonAPI.post(user_two, %{"status" => "This is also about 2hu"})
results =
conn
|> get("/api/v1/search", %{"q" => "2hu", "account_id" => user.id})
|> json_response(200)
assert [%{"id" => activity_id1}] = results["statuses"]
assert activity_id1 == activity1.id
assert [_] = results["accounts"]
results =
conn
|> get("/api/v1/search", %{"q" => "2hu", "account_id" => user_two.id})
|> json_response(200)
assert [%{"id" => activity_id2}] = results["statuses"]
assert activity_id2 == activity2.id
end
end
end
diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs
index 2de2725e0..a96fd860b 100644
--- a/test/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/web/mastodon_api/controllers/status_controller_test.exs
@@ -1,1311 +1,1343 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
+ clear_config([:instance, :federating])
+ clear_config([:instance, :allow_relay])
+
describe "posting statuses" do
setup do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
[conn: conn]
end
+ test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
+ Pleroma.Config.put([:instance, :federating], true)
+ Pleroma.Config.get([:instance, :allow_relay], true)
+ user = insert(:user)
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> post("api/v1/statuses", %{
+ "content_type" => "text/plain",
+ "source" => "Pleroma FE",
+ "status" => "Hello world",
+ "visibility" => "public"
+ })
+ |> json_response(200)
+
+ assert response["reblogs_count"] == 0
+ ObanHelpers.perform_all()
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> get("api/v1/statuses/#{response["id"]}", %{})
+ |> json_response(200)
+
+ assert response["reblogs_count"] == 0
+ end
+
test "posting a status", %{conn: conn} do
idempotency_key = "Pikachu rocks!"
conn_one =
conn
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
{:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key)
# Six hours
assert ttl > :timer.seconds(6 * 60 * 60 - 1)
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
json_response(conn_one, 200)
assert Activity.get_by_id(id)
conn_two =
conn
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
assert %{"id" => second_id} = json_response(conn_two, 200)
assert id == second_id
conn_three =
conn
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
assert %{"id" => third_id} = json_response(conn_three, 200)
refute id == third_id
# An activity that will expire:
# 2 hours
expires_in = 120 * 60
conn_four =
conn
|> post("api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200)
assert activity = Activity.get_by_id(fourth_id)
assert expiration = ActivityExpiration.get_by_activity_id(fourth_id)
estimated_expires_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(expires_in)
|> NaiveDateTime.truncate(:second)
# This assert will fail if the test takes longer than a minute. I sure hope it never does:
assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60
assert fourth_response["pleroma"]["expires_at"] ==
NaiveDateTime.to_iso8601(expiration.scheduled_at)
end
test "posting an undefined status with an attachment", %{conn: conn} do
user = insert(:user)
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)]
})
assert json_response(conn, 200)
end
test "replying to a status", %{conn: conn} do
user = insert(:user)
{:ok, replied_to} = CommonAPI.post(user, %{"status" => "cofe"})
conn =
conn
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
activity = Activity.get_by_id(id)
assert activity.data["context"] == replied_to.data["context"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
end
test "replying to a direct message with visibility other than direct", %{conn: conn} do
user = insert(:user)
{:ok, replied_to} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"})
Enum.each(["public", "private", "unlisted"], fn visibility ->
conn =
conn
|> post("/api/v1/statuses", %{
"status" => "@#{user.nickname} hey",
"in_reply_to_id" => replied_to.id,
"visibility" => visibility
})
assert json_response(conn, 422) == %{"error" => "The message visibility must be direct"}
end)
end
test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
conn =
conn
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a sensitive status", %{conn: conn} do
conn =
conn
|> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a fake status", %{conn: conn} do
real_conn =
conn
|> post("/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it"
})
real_status = json_response(real_conn, 200)
assert real_status
assert Object.get_by_ap_id(real_status["uri"])
real_status =
real_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
fake_conn =
conn
|> post("/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it",
"preview" => true
})
fake_status = json_response(fake_conn, 200)
assert fake_status
refute Object.get_by_ap_id(fake_status["uri"])
fake_status =
fake_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
assert real_status == fake_status
end
test "posting a status with OGP link preview", %{conn: conn} do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
Config.put([:rich_media, :enabled], true)
conn =
conn
|> post("/api/v1/statuses", %{
"status" => "https://example.com/ogp"
})
assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a direct status", %{conn: conn} do
user2 = insert(:user)
content = "direct cofe @#{user2.nickname}"
conn =
conn
|> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"})
assert %{"id" => id} = response = json_response(conn, 200)
assert response["visibility"] == "direct"
assert response["pleroma"]["direct_conversation_id"]
assert activity = Activity.get_by_id(id)
assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
assert activity.data["to"] == [user2.ap_id]
assert activity.data["cc"] == []
end
end
describe "posting scheduled statuses" do
test "creates a scheduled activity", %{conn: conn} do
user = insert(:user)
scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200)
assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
assert [] == Repo.all(Activity)
end
test "creates a scheduled activity with a media attachment", %{conn: conn} do
user = insert(:user)
scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)],
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200)
assert %{"type" => "image"} = media_attachment
end
test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
%{conn: conn} do
user = insert(:user)
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => scheduled_at
})
assert %{"content" => "not scheduled"} = json_response(conn, 200)
assert [] == Repo.all(ScheduledActivity)
end
test "returns error when daily user limit is exceeded", %{conn: conn} do
user = insert(:user)
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
assert %{"error" => "daily limit exceeded"} == json_response(conn, 422)
end
test "returns error when total user limit is exceeded", %{conn: conn} do
user = insert(:user)
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
tomorrow =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.hours(36), :millisecond)
|> NaiveDateTime.to_iso8601()
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
assert %{"error" => "total limit exceeded"} == json_response(conn, 422)
end
end
describe "posting polls" do
test "posting a poll", %{conn: conn} do
user = insert(:user)
time = NaiveDateTime.utc_now()
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "Who is the #bestgrill?",
"poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
})
response = json_response(conn, 200)
assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
title in ["Rei", "Asuka", "Misato"]
end)
assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
refute response["poll"]["expred"]
end
test "option limit is enforced", %{conn: conn} do
user = insert(:user)
limit = Config.get([:instance, :poll_limits, :max_options])
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
})
%{"error" => error} = json_response(conn, 422)
assert error == "Poll can't contain more than #{limit} options"
end
test "option character limit is enforced", %{conn: conn} do
user = insert(:user)
limit = Config.get([:instance, :poll_limits, :max_option_chars])
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "...",
"poll" => %{
"options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
"expires_in" => 1
}
})
%{"error" => error} = json_response(conn, 422)
assert error == "Poll options cannot be longer than #{limit} characters each"
end
test "minimal date limit is enforced", %{conn: conn} do
user = insert(:user)
limit = Config.get([:instance, :poll_limits, :min_expiration])
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit - 1
}
})
%{"error" => error} = json_response(conn, 422)
assert error == "Expiration date is too soon"
end
test "maximum date limit is enforced", %{conn: conn} do
user = insert(:user)
limit = Config.get([:instance, :poll_limits, :max_expiration])
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit + 1
}
})
%{"error" => error} = json_response(conn, 422)
assert error == "Expiration date is too far in the future"
end
end
test "get a status", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> get("/api/v1/statuses/#{activity.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(activity.id)
end
test "get a direct status", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "@#{other_user.nickname}", "visibility" => "direct"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}")
[participation] = Participation.for_user(user)
res = json_response(conn, 200)
assert res["pleroma"]["direct_conversation_id"] == participation.id
end
test "get statuses by IDs", %{conn: conn} do
%{id: id1} = insert(:note_activity)
%{id: id2} = insert(:note_activity)
query_string = "ids[]=#{id1}&ids[]=#{id2}"
conn = get(conn, "/api/v1/statuses/?#{query_string}")
assert [%{"id" => ^id1}, %{"id" => ^id2}] = Enum.sort_by(json_response(conn, :ok), & &1["id"])
end
describe "deleting a status" do
test "when you created it", %{conn: conn} do
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
conn =
conn
|> assign(:user, author)
|> delete("/api/v1/statuses/#{activity.id}")
assert %{} = json_response(conn, 200)
refute Activity.get_by_id(activity.id)
end
test "when you didn't create it", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/statuses/#{activity.id}")
assert %{"error" => _} = json_response(conn, 403)
assert Activity.get_by_id(activity.id) == activity
end
test "when you're an admin or moderator", %{conn: conn} do
activity1 = insert(:note_activity)
activity2 = insert(:note_activity)
- admin = insert(:user, info: %{is_admin: true})
- moderator = insert(:user, info: %{is_moderator: true})
+ admin = insert(:user, is_admin: true)
+ moderator = insert(:user, is_moderator: true)
res_conn =
conn
|> assign(:user, admin)
|> delete("/api/v1/statuses/#{activity1.id}")
assert %{} = json_response(res_conn, 200)
res_conn =
conn
|> assign(:user, moderator)
|> delete("/api/v1/statuses/#{activity2.id}")
assert %{} = json_response(res_conn, 200)
refute Activity.get_by_id(activity1.id)
refute Activity.get_by_id(activity2.id)
end
end
describe "reblogging" do
test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true
} = json_response(conn, 200)
assert to_string(activity.id) == id
end
test "reblogs privately and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/reblog", %{"visibility" => "private"})
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true,
"visibility" => "private"
} = json_response(conn, 200)
assert to_string(activity.id) == id
end
test "reblogged status for another user", %{conn: conn} do
activity = insert(:note_activity)
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
CommonAPI.favorite(activity.id, user2)
{:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
{:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
{:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
conn_res =
conn
|> assign(:user, user3)
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
"reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2},
"reblogged" => false,
"favourited" => false,
"bookmarked" => false
} = json_response(conn_res, 200)
conn_res =
conn
|> assign(:user, user2)
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
"reblogged" => true,
"favourited" => true,
"bookmarked" => true
} = json_response(conn_res, 200)
assert to_string(activity.id) == id
end
test "returns 400 error when activity is not exist", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/foo/reblog")
assert json_response(conn, 400) == %{"error" => "Could not repeat"}
end
end
describe "unreblogging" do
test "unreblogs and returns the unreblogged status", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unreblog")
assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200)
assert to_string(activity.id) == id
end
test "returns 400 error when activity is not exist", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/foo/unreblog")
assert json_response(conn, 400) == %{"error" => "Could not unrepeat"}
end
end
describe "favoriting" do
test "favs a status and returns it", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/favourite")
assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
json_response(conn, 200)
assert to_string(activity.id) == id
end
test "returns 400 error for a wrong id", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/1/favourite")
assert json_response(conn, 400) == %{"error" => "Could not favorite"}
end
end
describe "unfavoriting" do
test "unfavorites a status and returns it", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
{:ok, _, _} = CommonAPI.favorite(activity.id, user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
json_response(conn, 200)
assert to_string(activity.id) == id
end
test "returns 400 error for a wrong id", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/1/unfavourite")
assert json_response(conn, 400) == %{"error" => "Could not unfavorite"}
end
end
describe "pinned statuses" do
setup do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
[user: user, activity: activity]
end
clear_config([:instance, :max_pinned_statuses]) do
Config.put([:instance, :max_pinned_statuses], 1)
end
test "pin status", %{conn: conn, user: user, activity: activity} do
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "pinned" => true} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response(200)
assert [%{"id" => ^id_str, "pinned" => true}] =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response(200)
end
test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
{:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{dm.id}/pin")
assert json_response(conn, 400) == %{"error" => "Could not pin"}
end
test "unpin status", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.pin(activity.id, user)
id_str = to_string(activity.id)
user = refresh_record(user)
assert %{"id" => ^id_str, "pinned" => false} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unpin")
|> json_response(200)
assert [] =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response(200)
end
test "/unpin: returns 400 error when activity is not exist", %{conn: conn, user: user} do
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/1/unpin")
assert json_response(conn, 400) == %{"error" => "Could not unpin"}
end
test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
id_str_one = to_string(activity_one.id)
assert %{"id" => ^id_str_one, "pinned" => true} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{id_str_one}/pin")
|> json_response(200)
user = refresh_record(user)
assert %{"error" => "You have already pinned the maximum number of statuses"} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity_two.id}/pin")
|> json_response(400)
end
end
describe "cards" do
setup do
Config.put([:rich_media, :enabled], true)
user = insert(:user)
%{user: user}
end
test "returns rich-media card", %{conn: conn, user: user} do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} = CommonAPI.post(user, %{"status" => "https://example.com/ogp"})
card_data = %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"title" => "The Rock",
"type" => "link",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
"pleroma" => %{
"opengraph" => %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"type" => "video.movie",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
}
}
}
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response(200)
assert response == card_data
# works with private posts
{:ok, activity} =
CommonAPI.post(user, %{"status" => "https://example.com/ogp", "visibility" => "direct"})
response_two =
conn
|> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response(200)
assert response_two == card_data
end
test "replaces missing description with an empty string", %{conn: conn, user: user} do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "https://example.com/ogp-missing-data"})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response(:ok)
assert response == %{
"type" => "link",
"title" => "Pleroma",
"description" => "",
"image" => nil,
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"url" => "https://example.com/ogp-missing-data",
"pleroma" => %{
"opengraph" => %{
"title" => "Pleroma",
"type" => "website",
"url" => "https://example.com/ogp-missing-data"
}
}
}
end
end
test "bookmarks" do
user = insert(:user)
for_user = insert(:user)
{:ok, activity1} =
CommonAPI.post(user, %{
"status" => "heweoo?"
})
{:ok, activity2} =
CommonAPI.post(user, %{
"status" => "heweoo!"
})
response1 =
build_conn()
|> assign(:user, for_user)
|> post("/api/v1/statuses/#{activity1.id}/bookmark")
assert json_response(response1, 200)["bookmarked"] == true
response2 =
build_conn()
|> assign(:user, for_user)
|> post("/api/v1/statuses/#{activity2.id}/bookmark")
assert json_response(response2, 200)["bookmarked"] == true
bookmarks =
build_conn()
|> assign(:user, for_user)
|> get("/api/v1/bookmarks")
assert [json_response(response2, 200), json_response(response1, 200)] ==
json_response(bookmarks, 200)
response1 =
build_conn()
|> assign(:user, for_user)
|> post("/api/v1/statuses/#{activity1.id}/unbookmark")
assert json_response(response1, 200)["bookmarked"] == false
bookmarks =
build_conn()
|> assign(:user, for_user)
|> get("/api/v1/bookmarks")
assert [json_response(response2, 200)] == json_response(bookmarks, 200)
end
describe "conversation muting" do
setup do
post_user = insert(:user)
user = insert(:user)
{:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"})
[user: user, activity: activity]
end
test "mute conversation", %{conn: conn, user: user, activity: activity} do
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => true} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/mute")
|> json_response(200)
end
test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/mute")
assert json_response(conn, 400) == %{"error" => "conversation is already muted"}
end
test "unmute conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
id_str = to_string(activity.id)
user = refresh_record(user)
assert %{"id" => ^id_str, "muted" => false} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unmute")
|> json_response(200)
end
end
test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"})
# Reply to status from another user
conn1 =
conn
|> assign(:user, user2)
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response(conn1, 200)
activity = Activity.get_by_id_with_object(id)
assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
# Reblog from the third user
conn2 =
conn
|> assign(:user, user3)
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
json_response(conn2, 200)
assert to_string(activity.id) == id
# Getting third user status
conn3 =
conn
|> assign(:user, user3)
|> get("api/v1/timelines/home")
[reblogged_activity] = json_response(conn3, 200)
assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
end
describe "GET /api/v1/statuses/:id/favourited_by" do
setup do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
conn =
build_conn()
|> assign(:user, user)
[conn: conn, activity: activity, user: user]
end
test "returns users who have favorited the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been favorited yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "does not return users who have favorited the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
response =
conn
|> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
response =
conn
|> assign(:user, nil)
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentification for private posts", %{conn: conn, user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "@#{other_user.nickname} wanna get some #cofe together?",
"visibility" => "direct"
})
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
conn
|> assign(:user, nil)
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(404)
response =
build_conn()
|> assign(:user, other_user)
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(200)
[%{"id" => id}] = response
assert id == other_user.id
end
end
describe "GET /api/v1/statuses/:id/reblogged_by" do
setup do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
conn =
build_conn()
|> assign(:user, user)
[conn: conn, activity: activity, user: user]
end
test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been reblogged yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status privately", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user, %{"visibility" => "private"})
response =
conn
|> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> assign(:user, nil)
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentification for private posts", %{conn: conn, user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "@#{other_user.nickname} wanna get some #cofe together?",
"visibility" => "direct"
})
conn
|> assign(:user, nil)
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(404)
response =
build_conn()
|> assign(:user, other_user)
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(200)
assert [] == response
end
end
test "context" do
user = insert(:user)
{:ok, %{id: id1}} = CommonAPI.post(user, %{"status" => "1"})
{:ok, %{id: id2}} = CommonAPI.post(user, %{"status" => "2", "in_reply_to_status_id" => id1})
{:ok, %{id: id3}} = CommonAPI.post(user, %{"status" => "3", "in_reply_to_status_id" => id2})
{:ok, %{id: id4}} = CommonAPI.post(user, %{"status" => "4", "in_reply_to_status_id" => id3})
{:ok, %{id: id5}} = CommonAPI.post(user, %{"status" => "5", "in_reply_to_status_id" => id4})
response =
build_conn()
|> assign(:user, nil)
|> get("/api/v1/statuses/#{id3}/context")
|> json_response(:ok)
assert %{
"ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
"descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
} = response
end
test "returns the favorites of a user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
{:ok, _, _} = CommonAPI.favorite(activity.id, user)
first_conn =
conn
|> assign(:user, user)
|> get("/api/v1/favourites")
assert [status] = json_response(first_conn, 200)
assert status["id"] == to_string(activity.id)
assert [{"link", _link_header}] =
Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
# Honours query params
{:ok, second_activity} =
CommonAPI.post(other_user, %{
"status" =>
"Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
})
{:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
last_like = status["id"]
second_conn =
conn
|> assign(:user, user)
|> get("/api/v1/favourites?since_id=#{last_like}")
assert [second_status] = json_response(second_conn, 200)
assert second_status["id"] == to_string(second_activity.id)
third_conn =
conn
|> assign(:user, user)
|> get("/api/v1/favourites?limit=0")
assert [] = json_response(third_conn, 200)
end
end
diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs
index fc45c25de..61b6cea75 100644
--- a/test/web/mastodon_api/controllers/timeline_controller_test.exs
+++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs
@@ -1,316 +1,311 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
import Tesla.Mock
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.OStatus
clear_config([:instance, :public])
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "home" do
test "the home timeline", %{conn: conn} do
user = insert(:user)
following = insert(:user)
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/home")
assert Enum.empty?(json_response(conn, :ok))
{:ok, user} = User.follow(user, following)
conn =
build_conn()
|> assign(:user, user)
|> get("/api/v1/timelines/home")
assert [%{"content" => "test"}] = json_response(conn, :ok)
end
test "the home timeline when the direct messages are excluded", %{conn: conn} do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/home", %{"exclude_visibilities" => ["direct"]})
assert status_ids = json_response(conn, :ok) |> Enum.map(& &1["id"])
assert public_activity.id in status_ids
assert unlisted_activity.id in status_ids
assert private_activity.id in status_ids
refute direct_activity.id in status_ids
end
end
describe "public" do
@tag capture_log: true
test "the public timeline", %{conn: conn} do
following = insert(:user)
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
- {:ok, [_activity]} =
- OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
+ _activity = insert(:note_activity, local: false)
conn = get(conn, "/api/v1/timelines/public", %{"local" => "False"})
assert length(json_response(conn, :ok)) == 2
conn = get(build_conn(), "/api/v1/timelines/public", %{"local" => "True"})
assert [%{"content" => "test"}] = json_response(conn, :ok)
conn = get(build_conn(), "/api/v1/timelines/public", %{"local" => "1"})
assert [%{"content" => "test"}] = json_response(conn, :ok)
end
test "the public timeline when public is set to false", %{conn: conn} do
Config.put([:instance, :public], false)
assert %{"error" => "This resource requires authentication."} ==
conn
|> get("/api/v1/timelines/public", %{"local" => "False"})
|> json_response(:forbidden)
end
test "the public timeline includes only public statuses for an authenticated user" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "private"})
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "unlisted"})
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
res_conn = get(conn, "/api/v1/timelines/public")
assert length(json_response(res_conn, 200)) == 1
end
end
describe "direct" do
test "direct timeline", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, user_two} = User.follow(user_two, user_one)
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "direct"
})
{:ok, _follower_only} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "private"
})
# Only direct should be visible here
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/direct")
[status] = json_response(res_conn, :ok)
assert %{"visibility" => "direct"} = status
assert status["url"] != direct.data["id"]
# User should be able to see their own direct message
res_conn =
build_conn()
|> assign(:user, user_one)
|> get("api/v1/timelines/direct")
[status] = json_response(res_conn, :ok)
assert %{"visibility" => "direct"} = status
# Both should be visible here
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/home")
[_s1, _s2] = json_response(res_conn, :ok)
# Test pagination
Enum.each(1..20, fn _ ->
{:ok, _} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "direct"
})
end)
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/direct")
statuses = json_response(res_conn, :ok)
assert length(statuses) == 20
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]})
[status] = json_response(res_conn, :ok)
assert status["url"] != direct.data["id"]
end
test "doesn't include DMs from blocked users", %{conn: conn} do
blocker = insert(:user)
blocked = insert(:user)
user = insert(:user)
{:ok, blocker} = User.block(blocker, blocked)
{:ok, _blocked_direct} =
CommonAPI.post(blocked, %{
"status" => "Hi @#{blocker.nickname}!",
"visibility" => "direct"
})
{:ok, direct} =
CommonAPI.post(user, %{
"status" => "Hi @#{blocker.nickname}!",
"visibility" => "direct"
})
res_conn =
conn
|> assign(:user, user)
|> get("api/v1/timelines/direct")
[status] = json_response(res_conn, :ok)
assert status["id"] == direct.id
end
end
describe "list" do
test "list timeline", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity_one} = CommonAPI.post(user, %{"status" => "Marisa is cute."})
{:ok, activity_two} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response(conn, :ok)
assert id == to_string(activity_two.id)
end
test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity_one} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
{:ok, _activity_two} =
CommonAPI.post(other_user, %{
"status" => "Marisa is cute.",
"visibility" => "private"
})
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response(conn, :ok)
assert id == to_string(activity_one.id)
end
end
describe "hashtag" do
@tag capture_log: true
test "hashtag timeline", %{conn: conn} do
following = insert(:user)
{:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"})
- {:ok, [_activity]} =
- OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
-
nconn = get(conn, "/api/v1/timelines/tag/2hu")
assert [%{"id" => id}] = json_response(nconn, :ok)
assert id == to_string(activity.id)
# works for different capitalization too
nconn = get(conn, "/api/v1/timelines/tag/2HU")
assert [%{"id" => id}] = json_response(nconn, :ok)
assert id == to_string(activity.id)
end
test "multi-hashtag timeline", %{conn: conn} do
user = insert(:user)
{:ok, activity_test} = CommonAPI.post(user, %{"status" => "#test"})
{:ok, activity_test1} = CommonAPI.post(user, %{"status" => "#test #test1"})
{:ok, activity_none} = CommonAPI.post(user, %{"status" => "#test #none"})
any_test = get(conn, "/api/v1/timelines/tag/test", %{"any" => ["test1"]})
[status_none, status_test1, status_test] = json_response(any_test, :ok)
assert to_string(activity_test.id) == status_test["id"]
assert to_string(activity_test1.id) == status_test1["id"]
assert to_string(activity_none.id) == status_none["id"]
restricted_test =
get(conn, "/api/v1/timelines/tag/test", %{"all" => ["test1"], "none" => ["none"]})
assert [status_test1] == json_response(restricted_test, :ok)
all_test = get(conn, "/api/v1/timelines/tag/test", %{"all" => ["none"]})
assert [status_none] == json_response(all_test, :ok)
end
end
end
diff --git a/test/web/mastodon_api/mastodon_api_test.exs b/test/web/mastodon_api/mastodon_api_test.exs
index 7fcb2bd55..561ef05aa 100644
--- a/test/web/mastodon_api/mastodon_api_test.exs
+++ b/test/web/mastodon_api/mastodon_api_test.exs
@@ -1,104 +1,104 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPITest do
use Pleroma.Web.ConnCase
alias Pleroma.Notification
alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPI
import Pleroma.Factory
describe "follow/3" do
- test "returns error when user deactivated" do
+ test "returns error when followed user is deactivated" do
follower = insert(:user)
- user = insert(:user, local: true, info: %{deactivated: true})
+ user = insert(:user, local: true, deactivated: true)
{:error, error} = MastodonAPI.follow(follower, user)
- assert error == "Could not follow user: You are deactivated."
+ assert error == "Could not follow user: #{user.nickname} is deactivated."
end
test "following for user" do
follower = insert(:user)
user = insert(:user)
{:ok, follower} = MastodonAPI.follow(follower, user)
assert User.following?(follower, user)
end
test "returns ok if user already followed" do
follower = insert(:user)
user = insert(:user)
{:ok, follower} = User.follow(follower, user)
{:ok, follower} = MastodonAPI.follow(follower, refresh_record(user))
assert User.following?(follower, user)
end
end
describe "get_followers/2" do
test "returns user followers" do
follower1_user = insert(:user)
follower2_user = insert(:user)
user = insert(:user)
{:ok, _follower1_user} = User.follow(follower1_user, user)
{:ok, follower2_user} = User.follow(follower2_user, user)
assert MastodonAPI.get_followers(user, %{"limit" => 1}) == [follower2_user]
end
end
describe "get_friends/2" do
test "returns user friends" do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
followed_three = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
{:ok, user} = User.follow(user, followed_three)
res = MastodonAPI.get_friends(user)
assert length(res) == 3
assert Enum.member?(res, refresh_record(followed_three))
assert Enum.member?(res, refresh_record(followed_two))
assert Enum.member?(res, refresh_record(followed_one))
end
end
describe "get_notifications/2" do
test "returns notifications for user" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, status1} = CommonAPI.post(user, %{"status" => "Magi"})
{:ok, [notification]} = Notification.create_notifications(status)
{:ok, [notification1]} = Notification.create_notifications(status1)
res = MastodonAPI.get_notifications(subscriber)
assert Enum.member?(Enum.map(res, & &1.id), notification.id)
assert Enum.member?(Enum.map(res, & &1.id), notification1.id)
end
end
describe "get_scheduled_activities/2" do
test "returns user scheduled activities" do
user = insert(:user)
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
attrs = %{params: %{}, scheduled_at: today}
{:ok, schedule} = ScheduledActivity.create(user, attrs)
assert MastodonAPI.get_scheduled_activities(user) == [schedule]
end
end
end
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index b7a4938a6..af88841ed 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -1,518 +1,519 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
test "Represent a user account" do
source_data = %{
"tag" => [
%{
"type" => "Emoji",
"icon" => %{"url" => "/file.png"},
"name" => ":karjalanpiirakka:"
}
]
}
background_image = %{
"url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}]
}
user =
insert(:user, %{
- info: %{
- note_count: 5,
- follower_count: 3,
- source_data: source_data,
- background: background_image
- },
+ follower_count: 3,
+ note_count: 5,
+ source_data: source_data,
+ background: background_image,
nickname: "shp@shitposter.club",
name: ":karjalanpiirakka: shp",
bio: "<script src=\"invalid-html\"></script><span>valid html</span>",
inserted_at: ~N[2017-08-15 15:47:06.597036]
})
expected = %{
id: to_string(user.id),
username: "shp",
acct: user.nickname,
display_name: user.name,
locked: false,
created_at: "2017-08-15T15:47:06.000Z",
followers_count: 3,
following_count: 0,
statuses_count: 5,
note: "<span>valid html</span>",
url: user.ap_id,
avatar: "http://localhost:4001/images/avi.png",
avatar_static: "http://localhost:4001/images/avi.png",
header: "http://localhost:4001/images/banner.png",
header_static: "http://localhost:4001/images/banner.png",
emojis: [
%{
"static_url" => "/file.png",
"url" => "/file.png",
"shortcode" => "karjalanpiirakka",
"visible_in_picker" => false
}
],
fields: [],
bot: false,
source: %{
note: "valid html",
sensitive: false,
pleroma: %{
discoverable: false
},
fields: []
},
pleroma: %{
background_image: "https://example.com/images/asuka_hospital.png",
confirmation_pending: false,
tags: [],
is_admin: false,
is_moderator: false,
hide_favorites: true,
hide_followers: false,
hide_follows: false,
hide_followers_count: false,
hide_follows_count: false,
relationship: %{},
skip_thread_containment: false
}
}
assert expected == AccountView.render("show.json", %{user: user})
end
test "Represent the user account for the account owner" do
user = insert(:user)
notification_settings = %{
"followers" => true,
"follows" => true,
"non_follows" => true,
"non_followers" => true
}
- privacy = user.info.default_scope
+ privacy = user.default_scope
assert %{
pleroma: %{notification_settings: ^notification_settings},
source: %{privacy: ^privacy}
} = AccountView.render("show.json", %{user: user, for: user})
end
test "Represent a Service(bot) account" do
user =
insert(:user, %{
- info: %{note_count: 5, follower_count: 3, source_data: %{"type" => "Service"}},
+ follower_count: 3,
+ note_count: 5,
+ source_data: %{"type" => "Service"},
nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036]
})
expected = %{
id: to_string(user.id),
username: "shp",
acct: user.nickname,
display_name: user.name,
locked: false,
created_at: "2017-08-15T15:47:06.000Z",
followers_count: 3,
following_count: 0,
statuses_count: 5,
note: user.bio,
url: user.ap_id,
avatar: "http://localhost:4001/images/avi.png",
avatar_static: "http://localhost:4001/images/avi.png",
header: "http://localhost:4001/images/banner.png",
header_static: "http://localhost:4001/images/banner.png",
emojis: [],
fields: [],
bot: true,
source: %{
note: user.bio,
sensitive: false,
pleroma: %{
discoverable: false
},
fields: []
},
pleroma: %{
background_image: nil,
confirmation_pending: false,
tags: [],
is_admin: false,
is_moderator: false,
hide_favorites: true,
hide_followers: false,
hide_follows: false,
hide_followers_count: false,
hide_follows_count: false,
relationship: %{},
skip_thread_containment: false
}
}
assert expected == AccountView.render("show.json", %{user: user})
end
test "Represent a deactivated user for an admin" do
- admin = insert(:user, %{info: %{is_admin: true}})
- deactivated_user = insert(:user, %{info: %{deactivated: true}})
+ admin = insert(:user, is_admin: true)
+ deactivated_user = insert(:user, deactivated: true)
represented = AccountView.render("show.json", %{user: deactivated_user, for: admin})
assert represented[:pleroma][:deactivated] == true
end
test "Represent a smaller mention" do
user = insert(:user)
expected = %{
id: to_string(user.id),
acct: user.nickname,
username: user.nickname,
url: user.ap_id
}
assert expected == AccountView.render("mention.json", %{user: user})
end
describe "relationship" do
test "represent a relationship for the following and followed user" do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
{:ok, other_user} = User.follow(other_user, user)
{:ok, other_user} = User.subscribe(user, other_user)
{:ok, user} = User.mute(user, other_user, true)
{:ok, user} = CommonAPI.hide_reblogs(user, other_user)
expected = %{
id: to_string(other_user.id),
following: true,
followed_by: true,
blocking: false,
blocked_by: false,
muting: true,
muting_notifications: true,
subscribing: true,
requested: false,
domain_blocking: false,
showing_reblogs: false,
endorsed: false
}
assert expected ==
AccountView.render("relationship.json", %{user: user, target: other_user})
end
test "represent a relationship for the blocking and blocked user" do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
{:ok, other_user} = User.subscribe(user, other_user)
{:ok, user} = User.block(user, other_user)
{:ok, other_user} = User.block(other_user, user)
expected = %{
id: to_string(other_user.id),
following: false,
followed_by: false,
blocking: true,
blocked_by: true,
muting: false,
muting_notifications: false,
subscribing: false,
requested: false,
domain_blocking: false,
showing_reblogs: true,
endorsed: false
}
assert expected ==
AccountView.render("relationship.json", %{user: user, target: other_user})
end
test "represent a relationship for the user blocking a domain" do
user = insert(:user)
other_user = insert(:user, ap_id: "https://bad.site/users/other_user")
{:ok, user} = User.block_domain(user, "bad.site")
assert %{domain_blocking: true, blocking: false} =
AccountView.render("relationship.json", %{user: user, target: other_user})
end
test "represent a relationship for the user with a pending follow request" do
user = insert(:user)
- other_user = insert(:user, %{info: %User.Info{locked: true}})
+ other_user = insert(:user, locked: true)
{:ok, user, other_user, _} = CommonAPI.follow(user, other_user)
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
expected = %{
id: to_string(other_user.id),
following: false,
followed_by: false,
blocking: false,
blocked_by: false,
muting: false,
muting_notifications: false,
subscribing: false,
requested: true,
domain_blocking: false,
showing_reblogs: true,
endorsed: false
}
assert expected ==
AccountView.render("relationship.json", %{user: user, target: other_user})
end
end
test "represent an embedded relationship" do
user =
insert(:user, %{
- info: %{note_count: 5, follower_count: 0, source_data: %{"type" => "Service"}},
+ follower_count: 0,
+ note_count: 5,
+ source_data: %{"type" => "Service"},
nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036]
})
other_user = insert(:user)
{:ok, other_user} = User.follow(other_user, user)
{:ok, other_user} = User.block(other_user, user)
{:ok, _} = User.follow(insert(:user), user)
expected = %{
id: to_string(user.id),
username: "shp",
acct: user.nickname,
display_name: user.name,
locked: false,
created_at: "2017-08-15T15:47:06.000Z",
followers_count: 1,
following_count: 0,
statuses_count: 5,
note: user.bio,
url: user.ap_id,
avatar: "http://localhost:4001/images/avi.png",
avatar_static: "http://localhost:4001/images/avi.png",
header: "http://localhost:4001/images/banner.png",
header_static: "http://localhost:4001/images/banner.png",
emojis: [],
fields: [],
bot: true,
source: %{
note: user.bio,
sensitive: false,
pleroma: %{
discoverable: false
},
fields: []
},
pleroma: %{
background_image: nil,
confirmation_pending: false,
tags: [],
is_admin: false,
is_moderator: false,
hide_favorites: true,
hide_followers: false,
hide_follows: false,
hide_followers_count: false,
hide_follows_count: false,
relationship: %{
id: to_string(user.id),
following: false,
followed_by: false,
blocking: true,
blocked_by: false,
subscribing: false,
muting: false,
muting_notifications: false,
requested: false,
domain_blocking: false,
showing_reblogs: true,
endorsed: false
},
skip_thread_containment: false
}
}
assert expected == AccountView.render("show.json", %{user: user, for: other_user})
end
test "returns the settings store if the requesting user is the represented user and it's requested specifically" do
- user = insert(:user, %{info: %User.Info{pleroma_settings_store: %{fe: "test"}}})
+ user = insert(:user, pleroma_settings_store: %{fe: "test"})
result =
AccountView.render("show.json", %{user: user, for: user, with_pleroma_settings: true})
assert result.pleroma.settings_store == %{:fe => "test"}
result = AccountView.render("show.json", %{user: user, with_pleroma_settings: true})
assert result.pleroma[:settings_store] == nil
result = AccountView.render("show.json", %{user: user, for: user})
assert result.pleroma[:settings_store] == nil
end
test "sanitizes display names" do
user = insert(:user, name: "<marquee> username </marquee>")
result = AccountView.render("show.json", %{user: user})
refute result.display_name == "<marquee> username </marquee>"
end
describe "hiding follows/following" do
test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do
- info = %{
- hide_followers: true,
- hide_followers_count: true,
- hide_follows: true,
- hide_follows_count: true
- }
-
- user = insert(:user, info: info)
+ user =
+ insert(:user, %{
+ hide_followers: true,
+ hide_followers_count: true,
+ hide_follows: true,
+ hide_follows_count: true
+ })
other_user = insert(:user)
{:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{
followers_count: 0,
following_count: 0,
pleroma: %{hide_follows_count: true, hide_followers_count: true}
} = AccountView.render("show.json", %{user: user})
end
test "shows when follows/followers are hidden" do
- user = insert(:user, info: %{hide_followers: true, hide_follows: true})
+ user = insert(:user, hide_followers: true, hide_follows: true)
other_user = insert(:user)
{:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{
followers_count: 1,
following_count: 1,
pleroma: %{hide_follows: true, hide_followers: true}
} = AccountView.render("show.json", %{user: user})
end
test "shows actual follower/following count to the account owner" do
- user = insert(:user, info: %{hide_followers: true, hide_follows: true})
+ user = insert(:user, hide_followers: true, hide_follows: true)
other_user = insert(:user)
{:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{
followers_count: 1,
following_count: 1
} = AccountView.render("show.json", %{user: user, for: user})
end
test "shows unread_conversation_count only to the account owner" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
- CommonAPI.post(user, %{
- "status" => "Hey @#{other_user.nickname}.",
+ CommonAPI.post(other_user, %{
+ "status" => "Hey @#{user.nickname}.",
"visibility" => "direct"
})
user = User.get_cached_by_ap_id(user.ap_id)
assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][
:unread_conversation_count
] == nil
assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][
:unread_conversation_count
] == 1
end
end
describe "follow requests counter" do
test "shows zero when no follow requests are pending" do
user = insert(:user)
assert %{follow_requests_count: 0} =
AccountView.render("show.json", %{user: user, for: user})
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{follow_requests_count: 0} =
AccountView.render("show.json", %{user: user, for: user})
end
test "shows non-zero when follow requests are pending" do
- user = insert(:user, %{info: %{locked: true}})
+ user = insert(:user, locked: true)
assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{locked: true, follow_requests_count: 1} =
AccountView.render("show.json", %{user: user, for: user})
end
test "decreases when accepting a follow request" do
- user = insert(:user, %{info: %{locked: true}})
+ user = insert(:user, locked: true)
assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
other_user = insert(:user)
{:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{locked: true, follow_requests_count: 1} =
AccountView.render("show.json", %{user: user, for: user})
{:ok, _other_user} = CommonAPI.accept_follow_request(other_user, user)
assert %{locked: true, follow_requests_count: 0} =
AccountView.render("show.json", %{user: user, for: user})
end
test "decreases when rejecting a follow request" do
- user = insert(:user, %{info: %{locked: true}})
+ user = insert(:user, locked: true)
assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
other_user = insert(:user)
{:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{locked: true, follow_requests_count: 1} =
AccountView.render("show.json", %{user: user, for: user})
{:ok, _other_user} = CommonAPI.reject_follow_request(other_user, user)
assert %{locked: true, follow_requests_count: 0} =
AccountView.render("show.json", %{user: user, for: user})
end
test "shows non-zero when historical unapproved requests are present" do
- user = insert(:user, %{info: %{locked: true}})
+ user = insert(:user, locked: true)
assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
- {:ok, user} = User.update_info(user, &User.Info.user_upgrade(&1, %{locked: false}))
+ {:ok, user} = User.update_and_set_cache(user, %{locked: false})
assert %{locked: false, follow_requests_count: 1} =
AccountView.render("show.json", %{user: user, for: user})
end
end
end
diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs
new file mode 100644
index 000000000..8a5c89d56
--- /dev/null
+++ b/test/web/mastodon_api/views/marker_view_test.exs
@@ -0,0 +1,27 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
+ use Pleroma.DataCase
+ alias Pleroma.Web.MastodonAPI.MarkerView
+ import Pleroma.Factory
+
+ test "returns markers" do
+ marker1 = insert(:marker, timeline: "notifications", last_read_id: "17")
+ marker2 = insert(:marker, timeline: "home", last_read_id: "42")
+
+ assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{
+ "home" => %{
+ last_read_id: "42",
+ updated_at: NaiveDateTime.to_iso8601(marker2.updated_at),
+ version: 0
+ },
+ "notifications" => %{
+ last_read_id: "17",
+ updated_at: NaiveDateTime.to_iso8601(marker1.updated_at),
+ version: 0
+ }
+ }
+ end
+end
diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs
index 1d5a6e956..c200ad8fe 100644
--- a/test/web/mastodon_api/views/status_view_test.exs
+++ b/test/web/mastodon_api/views/status_view_test.exs
@@ -1,510 +1,507 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
- alias Pleroma.Web.OStatus
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "returns the direct conversation id when given the `with_conversation_id` option" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
status =
StatusView.render("show.json",
activity: activity,
with_direct_conversation_id: true,
for: user
)
assert status[:pleroma][:direct_conversation_id]
end
test "returns a temporary ap_id based user for activities missing db users" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
Repo.delete(user)
Cachex.clear(:user_cache)
%{account: ms_user} = StatusView.render("show.json", activity: activity)
assert ms_user.acct == "erroruser@example.com"
end
test "tries to get a user by nickname if fetching by ap_id doesn't work" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
{:ok, user} =
user
|> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"})
|> Repo.update()
Cachex.clear(:user_cache)
result = StatusView.render("show.json", activity: activity)
assert result[:account][:id] == to_string(user.id)
end
test "a note with null content" do
note = insert(:note_activity)
note_object = Object.normalize(note)
data =
note_object.data
|> Map.put("content", nil)
Object.change(note_object, %{data: data})
|> Object.update_and_set_cache()
User.get_cached_by_ap_id(note.data["actor"])
status = StatusView.render("show.json", %{activity: note})
assert status.content == ""
end
test "a note activity" do
note = insert(:note_activity)
object_data = Object.normalize(note).data
user = User.get_cached_by_ap_id(note.data["actor"])
convo_id = Utils.context_to_conversation_id(object_data["context"])
status = StatusView.render("show.json", %{activity: note})
created_at =
(object_data["published"] || "")
|> String.replace(~r/\.\d+Z/, ".000Z")
expected = %{
id: to_string(note.id),
uri: object_data["id"],
url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note),
account: AccountView.render("show.json", %{user: user}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
card: nil,
reblog: nil,
content: HtmlSanitizeEx.basic_html(object_data["content"]),
created_at: created_at,
reblogs_count: 0,
replies_count: 0,
favourites_count: 0,
reblogged: false,
bookmarked: false,
favourited: false,
muted: false,
pinned: false,
sensitive: false,
poll: nil,
spoiler_text: HtmlSanitizeEx.basic_html(object_data["summary"]),
visibility: "public",
media_attachments: [],
mentions: [],
tags: [
%{
name: "#{object_data["tag"]}",
url: "/tag/#{object_data["tag"]}"
}
],
application: %{
name: "Web",
website: nil
},
language: nil,
emojis: [
%{
shortcode: "2hu",
url: "corndog.png",
static_url: "corndog.png",
visible_in_picker: false
}
],
pleroma: %{
local: true,
conversation_id: convo_id,
in_reply_to_account_acct: nil,
content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])},
spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])},
expires_at: nil,
direct_conversation_id: nil,
thread_muted: false
}
}
assert status == expected
end
test "tells if the message is muted for some reason" do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
status = StatusView.render("show.json", %{activity: activity})
assert status.muted == false
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.muted == true
end
test "tells if the message is thread muted" do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.pleroma.thread_muted == false
{:ok, activity} = CommonAPI.add_mute(user, activity)
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.pleroma.thread_muted == true
end
test "tells if the status is bookmarked" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Cute girls doing cute things"})
status = StatusView.render("show.json", %{activity: activity})
assert status.bookmarked == false
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.bookmarked == false
{:ok, _bookmark} = Bookmark.create(user.id, activity.id)
activity = Activity.get_by_id_with_object(activity.id)
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.bookmarked == true
end
test "a reply" do
note = insert(:note_activity)
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "he", "in_reply_to_status_id" => note.id})
status = StatusView.render("show.json", %{activity: activity})
assert status.in_reply_to_id == to_string(note.id)
[status] = StatusView.render("index.json", %{activities: [activity], as: :activity})
assert status.in_reply_to_id == to_string(note.id)
end
test "contains mentions" do
- incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
- # a user with this ap id might be in the cache.
- recipient = "https://pleroma.soykaf.com/users/lain"
- user = insert(:user, %{ap_id: recipient})
+ user = insert(:user)
+ mentioned = insert(:user)
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hi @#{mentioned.nickname}"})
status = StatusView.render("show.json", %{activity: activity})
assert status.mentions ==
- Enum.map([user], fn u -> AccountView.render("mention.json", %{user: u}) end)
+ Enum.map([mentioned], fn u -> AccountView.render("mention.json", %{user: u}) end)
end
test "create mentions from the 'to' field" do
%User{ap_id: recipient_ap_id} = insert(:user)
cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
object =
insert(:note, %{
data: %{
"to" => [recipient_ap_id],
"cc" => cc
}
})
activity =
insert(:note_activity, %{
note: object,
recipients: [recipient_ap_id | cc]
})
assert length(activity.recipients) == 3
%{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
assert length(mentions) == 1
assert mention.url == recipient_ap_id
end
test "create mentions from the 'tag' field" do
recipient = insert(:user)
cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
object =
insert(:note, %{
data: %{
"cc" => cc,
"tag" => [
%{
"href" => recipient.ap_id,
"name" => recipient.nickname,
"type" => "Mention"
},
%{
"href" => "https://example.com/search?tag=test",
"name" => "#test",
"type" => "Hashtag"
}
]
}
})
activity =
insert(:note_activity, %{
note: object,
recipients: [recipient.ap_id | cc]
})
assert length(activity.recipients) == 3
%{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
assert length(mentions) == 1
assert mention.url == recipient.ap_id
end
test "attachments" do
object = %{
"type" => "Image",
"url" => [
%{
"mediaType" => "image/png",
"href" => "someurl"
}
],
"uuid" => 6
}
expected = %{
id: "1638338801",
type: "image",
url: "someurl",
remote_url: "someurl",
preview_url: "someurl",
text_url: "someurl",
description: nil,
pleroma: %{mime_type: "image/png"}
}
assert expected == StatusView.render("attachment.json", %{attachment: object})
# If theres a "id", use that instead of the generated one
object = Map.put(object, "id", 2)
assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object})
end
test "put the url advertised in the Activity in to the url attribute" do
id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810"
[activity] = Activity.search(nil, id)
status = StatusView.render("show.json", %{activity: activity})
assert status.uri == id
assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/"
end
test "a reblog" do
user = insert(:user)
activity = insert(:note_activity)
{:ok, reblog, _} = CommonAPI.repeat(activity.id, user)
represented = StatusView.render("show.json", %{for: user, activity: reblog})
assert represented[:id] == to_string(reblog.id)
assert represented[:reblog][:id] == to_string(activity.id)
assert represented[:emojis] == []
end
test "a peertube video" do
user = insert(:user)
{:ok, object} =
Pleroma.Object.Fetcher.fetch_object_from_id(
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
)
%Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
represented = StatusView.render("show.json", %{for: user, activity: activity})
assert represented[:id] == to_string(activity.id)
assert length(represented[:media_attachments]) == 1
end
describe "build_tags/1" do
test "it returns a a dictionary tags" do
object_tags = [
"fediverse",
"mastodon",
"nextcloud",
%{
"href" => "https://kawen.space/users/lain",
"name" => "@lain@kawen.space",
"type" => "Mention"
}
]
assert StatusView.build_tags(object_tags) == [
%{name: "fediverse", url: "/tag/fediverse"},
%{name: "mastodon", url: "/tag/mastodon"},
%{name: "nextcloud", url: "/tag/nextcloud"}
]
end
end
describe "rich media cards" do
test "a rich media card without a site name renders correctly" do
page_url = "http://example.com"
card = %{
url: page_url,
image: page_url <> "/example.jpg",
title: "Example website"
}
%{provider_name: "example.com"} =
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end
test "a rich media card without a site name or image renders correctly" do
page_url = "http://example.com"
card = %{
url: page_url,
title: "Example website"
}
%{provider_name: "example.com"} =
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end
test "a rich media card without an image renders correctly" do
page_url = "http://example.com"
card = %{
url: page_url,
site_name: "Example site name",
title: "Example website"
}
%{provider_name: "Example site name"} =
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end
test "a rich media card with all relevant data renders correctly" do
page_url = "http://example.com"
card = %{
url: page_url,
site_name: "Example site name",
title: "Example website",
image: page_url <> "/example.jpg",
description: "Example description"
}
%{provider_name: "Example site name"} =
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end
end
test "embeds a relationship in the account" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "drink more water"
})
result = StatusView.render("show.json", %{activity: activity, for: other_user})
assert result[:account][:pleroma][:relationship] ==
AccountView.render("relationship.json", %{user: other_user, target: user})
end
test "embeds a relationship in the account in reposts" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "˙˙ɐʎns"
})
{:ok, activity, _object} = CommonAPI.repeat(activity.id, other_user)
result = StatusView.render("show.json", %{activity: activity, for: user})
assert result[:account][:pleroma][:relationship] ==
AccountView.render("relationship.json", %{user: user, target: other_user})
assert result[:reblog][:account][:pleroma][:relationship] ==
AccountView.render("relationship.json", %{user: user, target: user})
end
test "visibility/list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
status = StatusView.render("show.json", activity: activity)
assert status.visibility == "list"
end
test "successfully renders a Listen activity (pleroma extension)" do
listen_activity = insert(:listen)
status = StatusView.render("listen.json", activity: listen_activity)
assert status.length == listen_activity.data["object"]["length"]
assert status.title == listen_activity.data["object"]["title"]
end
end
diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs
index e15a0bfff..a3281b25b 100644
--- a/test/web/node_info_test.exs
+++ b/test/web/node_info_test.exs
@@ -1,137 +1,137 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.NodeInfoTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
test "GET /.well-known/nodeinfo", %{conn: conn} do
links =
conn
|> get("/.well-known/nodeinfo")
|> json_response(200)
|> Map.fetch!("links")
Enum.each(links, fn link ->
href = Map.fetch!(link, "href")
conn
|> get(href)
|> json_response(200)
end)
end
test "nodeinfo shows staff accounts", %{conn: conn} do
- moderator = insert(:user, %{local: true, info: %{is_moderator: true}})
- admin = insert(:user, %{local: true, info: %{is_admin: true}})
+ moderator = insert(:user, local: true, is_moderator: true)
+ admin = insert(:user, local: true, is_admin: true)
conn =
conn
|> get("/nodeinfo/2.1.json")
assert result = json_response(conn, 200)
assert moderator.ap_id in result["metadata"]["staffAccounts"]
assert admin.ap_id in result["metadata"]["staffAccounts"]
end
test "nodeinfo shows restricted nicknames", %{conn: conn} do
conn =
conn
|> get("/nodeinfo/2.1.json")
assert result = json_response(conn, 200)
assert Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) ==
result["metadata"]["restrictedNicknames"]
end
test "returns software.repository field in nodeinfo 2.1", %{conn: conn} do
conn
|> get("/.well-known/nodeinfo")
|> json_response(200)
conn =
conn
|> get("/nodeinfo/2.1.json")
assert result = json_response(conn, 200)
assert Pleroma.Application.repository() == result["software"]["repository"]
end
test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do
option = Pleroma.Config.get([:instance, :safe_dm_mentions])
Pleroma.Config.put([:instance, :safe_dm_mentions], true)
response =
conn
|> get("/nodeinfo/2.1.json")
|> json_response(:ok)
assert "safe_dm_mentions" in response["metadata"]["features"]
Pleroma.Config.put([:instance, :safe_dm_mentions], false)
response =
conn
|> get("/nodeinfo/2.1.json")
|> json_response(:ok)
refute "safe_dm_mentions" in response["metadata"]["features"]
Pleroma.Config.put([:instance, :safe_dm_mentions], option)
end
test "it shows MRF transparency data if enabled", %{conn: conn} do
config = Pleroma.Config.get([:instance, :rewrite_policy])
Pleroma.Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
option = Pleroma.Config.get([:instance, :mrf_transparency])
Pleroma.Config.put([:instance, :mrf_transparency], true)
simple_config = %{"reject" => ["example.com"]}
Pleroma.Config.put(:mrf_simple, simple_config)
response =
conn
|> get("/nodeinfo/2.1.json")
|> json_response(:ok)
assert response["metadata"]["federation"]["mrf_simple"] == simple_config
Pleroma.Config.put([:instance, :rewrite_policy], config)
Pleroma.Config.put([:instance, :mrf_transparency], option)
Pleroma.Config.put(:mrf_simple, %{})
end
test "it performs exclusions from MRF transparency data if configured", %{conn: conn} do
config = Pleroma.Config.get([:instance, :rewrite_policy])
Pleroma.Config.put([:instance, :rewrite_policy], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
option = Pleroma.Config.get([:instance, :mrf_transparency])
Pleroma.Config.put([:instance, :mrf_transparency], true)
exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
Pleroma.Config.put([:instance, :mrf_transparency_exclusions], ["other.site"])
simple_config = %{"reject" => ["example.com", "other.site"]}
expected_config = %{"reject" => ["example.com"]}
Pleroma.Config.put(:mrf_simple, simple_config)
response =
conn
|> get("/nodeinfo/2.1.json")
|> json_response(:ok)
assert response["metadata"]["federation"]["mrf_simple"] == expected_config
assert response["metadata"]["federation"]["exclusions"] == true
Pleroma.Config.put([:instance, :rewrite_policy], config)
Pleroma.Config.put([:instance, :mrf_transparency], option)
Pleroma.Config.put([:instance, :mrf_transparency_exclusions], exclusions)
Pleroma.Config.put(:mrf_simple, %{})
end
end
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index 41aaf6189..ad8d79083 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -1,1066 +1,1066 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.Token
@session_opts [
store: :cookie,
key: "_test",
signing_salt: "cooldude"
]
clear_config_all([:instance, :account_activation_required])
describe "in OAuth consumer mode, " do
setup do
[
app: insert(:oauth_app),
conn:
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
]
end
clear_config([:auth, :oauth_consumer_strategies]) do
Pleroma.Config.put(
[:auth, :oauth_consumer_strategies],
~w(twitter facebook)
)
end
test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
)
assert response = html_response(conn, 200)
assert response =~ "Sign in with Twitter"
assert response =~ o_auth_path(conn, :prepare_request)
end
test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/prepare_request",
%{
"provider" => "twitter",
"authorization" => %{
"scope" => "read follow",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state"
}
}
)
assert response = html_response(conn, 302)
redirect_query = URI.parse(redirected_to(conn)).query
assert %{"state" => state_param} = URI.decode_query(redirect_query)
assert {:ok, state_components} = Poison.decode(state_param)
expected_client_id = app.client_id
expected_redirect_uri = app.redirect_uris
assert %{
"scope" => "read follow",
"client_id" => ^expected_client_id,
"redirect_uri" => ^expected_redirect_uri,
"state" => "a_state"
} = state_components
end
test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`",
%{app: app, conn: conn} do
registration = insert(:registration)
redirect_uri = OAuthController.default_redirect_uri(app)
state_params = %{
"scope" => Enum.join(app.scopes, " "),
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => ""
}
conn =
conn
|> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid})
|> get(
"/oauth/twitter/callback",
%{
"oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
"oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
"provider" => "twitter",
"state" => Poison.encode!(state_params)
}
)
assert response = html_response(conn, 302)
assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
end
test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page",
%{app: app, conn: conn} do
user = insert(:user)
state_params = %{
"scope" => "read write",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state"
}
conn =
conn
|> assign(:ueberauth_auth, %{
provider: "twitter",
uid: "171799000",
info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil}
})
|> get(
"/oauth/twitter/callback",
%{
"oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
"oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
"provider" => "twitter",
"state" => Poison.encode!(state_params)
}
)
assert response = html_response(conn, 200)
assert response =~ ~r/name="op" type="submit" value="register"/
assert response =~ ~r/name="op" type="submit" value="connect"/
assert response =~ user.email
assert response =~ user.nickname
end
test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{
app: app,
conn: conn
} do
state_params = %{
"scope" => Enum.join(app.scopes, " "),
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => ""
}
conn =
conn
|> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]})
|> get(
"/oauth/twitter/callback",
%{
"oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
"oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
"provider" => "twitter",
"state" => Poison.encode!(state_params)
}
)
assert response = html_response(conn, 302)
assert redirected_to(conn) == app.redirect_uris
assert get_flash(conn, :error) == "Failed to authenticate: (error description)."
end
test "GET /oauth/registration_details renders registration details form", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/registration_details",
%{
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state",
"nickname" => nil,
"email" => "john@doe.com"
}
}
)
assert response = html_response(conn, 200)
assert response =~ ~r/name="op" type="submit" value="register"/
assert response =~ ~r/name="op" type="submit" value="connect"/
end
test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`",
%{
app: app,
conn: conn
} do
registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
redirect_uri = OAuthController.default_redirect_uri(app)
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "register",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "a_state",
"nickname" => "availablenick",
"email" => "available@email.com"
}
}
)
assert response = html_response(conn, 302)
assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
end
test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in HTTP 401",
%{
app: app,
conn: conn
} do
registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
unlisted_redirect_uri = "http://cross-site-request.com"
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "register",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => unlisted_redirect_uri,
"state" => "a_state",
"nickname" => "availablenick",
"email" => "available@email.com"
}
}
)
assert response = html_response(conn, 401)
end
test "with invalid params, POST /oauth/register?op=register renders registration_details page",
%{
app: app,
conn: conn
} do
another_user = insert(:user)
registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
params = %{
"op" => "register",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state",
"nickname" => "availablenickname",
"email" => "available@email.com"
}
}
for {bad_param, bad_param_value} <-
[{"nickname", another_user.nickname}, {"email", another_user.email}] do
bad_registration_attrs = %{
"authorization" => Map.put(params["authorization"], bad_param, bad_param_value)
}
bad_params = Map.merge(params, bad_registration_attrs)
conn =
conn
|> put_session(:registration_id, registration.id)
|> post("/oauth/register", bad_params)
assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/
assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken."
end
end
test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`",
%{
app: app,
conn: conn
} do
user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt("testpassword"))
registration = insert(:registration, user: nil)
redirect_uri = OAuthController.default_redirect_uri(app)
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "connect",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "a_state",
"name" => user.nickname,
"password" => "testpassword"
}
}
)
assert response = html_response(conn, 302)
assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
end
test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in HTTP 401`",
%{
app: app,
conn: conn
} do
user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt("testpassword"))
registration = insert(:registration, user: nil)
unlisted_redirect_uri = "http://cross-site-request.com"
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "connect",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => unlisted_redirect_uri,
"state" => "a_state",
"name" => user.nickname,
"password" => "testpassword"
}
}
)
assert response = html_response(conn, 401)
end
test "with invalid params, POST /oauth/register?op=connect renders registration_details page",
%{
app: app,
conn: conn
} do
user = insert(:user)
registration = insert(:registration, user: nil)
params = %{
"op" => "connect",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state",
"name" => user.nickname,
"password" => "wrong password"
}
}
conn =
conn
|> put_session(:registration_id, registration.id)
|> post("/oauth/register", params)
assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/
assert get_flash(conn, :error) == "Invalid Username/Password"
end
end
describe "GET /oauth/authorize" do
setup do
[
app: insert(:oauth_app, redirect_uris: "https://redirect.url"),
conn:
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
]
end
test "renders authentication page", %{app: app, conn: conn} do
conn =
get(
conn,
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "properly handles internal calls with `authorization`-wrapped params", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/authorize",
%{
"authorization" => %{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "renders authentication page if user is already authenticated but `force_login` is tru-ish",
%{app: app, conn: conn} do
token = insert(:oauth_token, app_id: app.id)
conn =
conn
|> put_session(:oauth_token, token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read",
"force_login" => "true"
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params",
%{
app: app,
conn: conn
} do
token = insert(:oauth_token, app_id: app.id)
conn =
conn
|> put_session(:oauth_token, token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "specific_client_state",
"scope" => "read"
}
)
assert URI.decode(redirected_to(conn)) ==
"https://redirect.url?access_token=#{token.token}&state=specific_client_state"
end
test "with existing authentication and unlisted non-OOB `redirect_uri`, redirects without credentials",
%{
app: app,
conn: conn
} do
unlisted_redirect_uri = "http://cross-site-request.com"
token = insert(:oauth_token, app_id: app.id)
conn =
conn
|> put_session(:oauth_token, token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => unlisted_redirect_uri,
"state" => "specific_client_state",
"scope" => "read"
}
)
assert redirected_to(conn) == unlisted_redirect_uri
end
test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params",
%{
app: app,
conn: conn
} do
token = insert(:oauth_token, app_id: app.id)
conn =
conn
|> put_session(:oauth_token, token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
"scope" => "read"
}
)
assert html_response(conn, 200) =~ "Authorization exists"
end
end
describe "POST /oauth/authorize" do
test "redirects with oauth authorization" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write", "follow"])
redirect_uri = OAuthController.default_redirect_uri(app)
conn =
build_conn()
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"scope" => "read:subscope write",
"state" => "statepassed"
}
})
target = redirected_to(conn)
assert target =~ redirect_uri
query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
assert %{"state" => "statepassed", "code" => code} = query
auth = Repo.get_by(Authorization, token: code)
assert auth
assert auth.scopes == ["read:subscope", "write"]
end
test "returns 401 for wrong credentials", %{conn: conn} do
user = insert(:user)
app = insert(:oauth_app)
redirect_uri = OAuthController.default_redirect_uri(app)
result =
conn
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "wrong",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "statepassed",
"scope" => Enum.join(app.scopes, " ")
}
})
|> html_response(:unauthorized)
# Keep the details
assert result =~ app.client_id
assert result =~ redirect_uri
# Error message
assert result =~ "Invalid Username/Password"
end
test "returns 401 for missing scopes", %{conn: conn} do
user = insert(:user)
app = insert(:oauth_app)
redirect_uri = OAuthController.default_redirect_uri(app)
result =
conn
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "statepassed",
"scope" => ""
}
})
|> html_response(:unauthorized)
# Keep the details
assert result =~ app.client_id
assert result =~ redirect_uri
# Error message
assert result =~ "This action is outside the authorized scopes"
end
test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
redirect_uri = OAuthController.default_redirect_uri(app)
result =
conn
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "statepassed",
"scope" => "read write follow"
}
})
|> html_response(:unauthorized)
# Keep the details
assert result =~ app.client_id
assert result =~ redirect_uri
# Error message
assert result =~ "This action is outside the authorized scopes"
end
end
describe "POST /oauth/token" do
test "issues a token for an all-body request" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200)
token = Repo.get_by(Token, token: token)
assert token
assert token.scopes == auth.scopes
assert user.ap_id == ap_id
end
test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do
password = "testpassword"
user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
app = insert(:oauth_app, scopes: ["read", "write"])
# Note: "scope" param is intentionally omitted
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token} = json_response(conn, 200)
token = Repo.get_by(Token, token: token)
assert token
assert token.scopes == app.scopes
end
test "issues a token for request with HTTP basic auth client credentials" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"])
{:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"])
assert auth.scopes == ["scope1", "scope2"]
app_encoded =
(URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret))
|> Base.encode64()
conn =
build_conn()
|> put_req_header("authorization", "Basic " <> app_encoded)
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => OAuthController.default_redirect_uri(app)
})
assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200)
assert scope == "scope1 scope2"
token = Repo.get_by(Token, token: token)
assert token
assert token.scopes == ["scope1", "scope2"]
end
test "issue a token for client_credentials grant type" do
app = insert(:oauth_app, scopes: ["read", "write"])
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "client_credentials",
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
json_response(conn, 200)
assert token
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
assert refresh
assert scope == "read write"
end
test "rejects token exchange with invalid client credentials" do
user = insert(:user)
app = insert(:oauth_app)
{:ok, auth} = Authorization.create_authorization(app, user)
conn =
build_conn()
|> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=")
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => OAuthController.default_redirect_uri(app)
})
assert resp = json_response(conn, 400)
assert %{"error" => _} = resp
refute Map.has_key?(resp, "access_token")
end
test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
Pleroma.Config.put([:instance, :account_activation_required], true)
password = "testpassword"
{:ok, user} =
insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
- |> User.change_info(&User.Info.confirmation_changeset(&1, need_confirmation: true))
- |> Repo.update()
+ |> User.confirmation_changeset(need_confirmation: true)
+ |> User.update_and_set_cache()
refute Pleroma.User.auth_active?(user)
app = insert(:oauth_app)
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 403)
assert %{"error" => _} = resp
refute Map.has_key?(resp, "access_token")
end
test "rejects token exchange for valid credentials belonging to deactivated user" do
password = "testpassword"
user =
insert(:user,
password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
- info: %{deactivated: true}
+ deactivated: true
)
app = insert(:oauth_app)
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 403)
assert %{"error" => _} = resp
refute Map.has_key?(resp, "access_token")
end
test "rejects token exchange for user with password_reset_pending set to true" do
password = "testpassword"
user =
insert(:user,
password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
- info: %{password_reset_pending: true}
+ password_reset_pending: true
)
app = insert(:oauth_app, scopes: ["read", "write"])
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 403)
assert resp["error"] == "Password reset is required"
assert resp["identifier"] == "password_reset_required"
refute Map.has_key?(resp, "access_token")
end
test "rejects an invalid authorization code" do
app = insert(:oauth_app)
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => "Imobviouslyinvalid",
"redirect_uri" => OAuthController.default_redirect_uri(app),
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 400)
assert %{"error" => _} = json_response(conn, 400)
refute Map.has_key?(resp, "access_token")
end
end
describe "POST /oauth/token - refresh token" do
clear_config([:oauth2, :issue_new_refresh_token])
test "issues a new access token with keep fresh token" do
Pleroma.Config.put([:oauth2, :issue_new_refresh_token], true)
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => token.refresh_token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(200)
ap_id = user.ap_id
assert match?(
%{
"scope" => "write",
"token_type" => "Bearer",
"expires_in" => 600,
"access_token" => _,
"refresh_token" => _,
"me" => ^ap_id
},
response
)
refute Repo.get_by(Token, token: token.token)
new_token = Repo.get_by(Token, token: response["access_token"])
assert new_token.refresh_token == token.refresh_token
assert new_token.scopes == auth.scopes
assert new_token.user_id == user.id
assert new_token.app_id == app.id
end
test "issues a new access token with new fresh token" do
Pleroma.Config.put([:oauth2, :issue_new_refresh_token], false)
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => token.refresh_token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(200)
ap_id = user.ap_id
assert match?(
%{
"scope" => "write",
"token_type" => "Bearer",
"expires_in" => 600,
"access_token" => _,
"refresh_token" => _,
"me" => ^ap_id
},
response
)
refute Repo.get_by(Token, token: token.token)
new_token = Repo.get_by(Token, token: response["access_token"])
refute new_token.refresh_token == token.refresh_token
assert new_token.scopes == auth.scopes
assert new_token.user_id == user.id
assert new_token.app_id == app.id
end
test "returns 400 if we try use access token" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => token.token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(400)
assert %{"error" => "Invalid credentials"} == response
end
test "returns 400 if refresh_token invalid" do
app = insert(:oauth_app, scopes: ["read", "write"])
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => "token.refresh_token",
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(400)
assert %{"error" => "Invalid credentials"} == response
end
test "issues a new token if token expired" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
change =
Ecto.Changeset.change(
token,
%{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)}
)
{:ok, access_token} = Repo.update(change)
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => access_token.refresh_token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(200)
ap_id = user.ap_id
assert match?(
%{
"scope" => "write",
"token_type" => "Bearer",
"expires_in" => 600,
"access_token" => _,
"refresh_token" => _,
"me" => ^ap_id
},
response
)
refute Repo.get_by(Token, token: token.token)
token = Repo.get_by(Token, token: response["access_token"])
assert token
assert token.scopes == auth.scopes
assert token.user_id == user.id
assert token.app_id == app.id
end
end
describe "POST /oauth/token - bad request" do
test "returns 500" do
response =
build_conn()
|> post("/oauth/token", %{})
|> json_response(500)
assert %{"error" => "Bad request"} == response
end
end
describe "POST /oauth/revoke - bad request" do
test "returns 500" do
response =
build_conn()
|> post("/oauth/revoke", %{})
|> json_response(500)
assert %{"error" => "Bad request"} == response
end
end
end
diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs
deleted file mode 100644
index a8d500890..000000000
--- a/test/web/ostatus/activity_representer_test.exs
+++ /dev/null
@@ -1,300 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OStatus.ActivityRepresenterTest do
- use Pleroma.DataCase
-
- alias Pleroma.Activity
- alias Pleroma.Object
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.OStatus.ActivityRepresenter
-
- import Pleroma.Factory
- import Tesla.Mock
-
- setup do
- mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- test "an external note activity" do
- incoming = File.read!("test/fixtures/mastodon-note-cw.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
-
- user = User.get_cached_by_ap_id(activity.data["actor"])
-
- tuple = ActivityRepresenter.to_simple_form(activity, user)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- assert String.contains?(
- res,
- ~s{<link type="text/html" href="https://mastodon.social/users/lambadalambda/updates/2314748" rel="alternate"/>}
- )
- end
-
- test "a note activity" do
- note_activity = insert(:note_activity)
- object_data = Object.normalize(note_activity).data
-
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
- <id>#{object_data["id"]}</id>
- <title>New note by #{user.nickname}</title>
- <content type="html">#{object_data["content"]}</content>
- <published>#{object_data["published"]}</published>
- <updated>#{object_data["published"]}</updated>
- <ostatus:conversation ref="#{note_activity.data["context"]}">#{note_activity.data["context"]}</ostatus:conversation>
- <link ref="#{note_activity.data["context"]}" rel="ostatus:conversation" />
- <summary>#{object_data["summary"]}</summary>
- <link type="application/atom+xml" href="#{object_data["id"]}" rel="self" />
- <link type="text/html" href="#{object_data["id"]}" rel="alternate" />
- <category term="2hu"/>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
- <link name="2hu" rel="emoji" href="corndog.png" />
- """
-
- tuple = ActivityRepresenter.to_simple_form(note_activity, user)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- assert clean(res) == clean(expected)
- end
-
- test "a reply note" do
- user = insert(:user)
- note_object = insert(:note)
- _note = insert(:note_activity, %{note: note_object})
- object = insert(:note, %{data: %{"inReplyTo" => note_object.data["id"]}})
- answer = insert(:note_activity, %{note: object})
-
- Repo.update!(
- Object.change(note_object, %{data: Map.put(note_object.data, "external_url", "someurl")})
- )
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
- <id>#{object.data["id"]}</id>
- <title>New note by #{user.nickname}</title>
- <content type="html">#{object.data["content"]}</content>
- <published>#{object.data["published"]}</published>
- <updated>#{object.data["published"]}</updated>
- <ostatus:conversation ref="#{answer.data["context"]}">#{answer.data["context"]}</ostatus:conversation>
- <link ref="#{answer.data["context"]}" rel="ostatus:conversation" />
- <summary>2hu</summary>
- <link type="application/atom+xml" href="#{object.data["id"]}" rel="self" />
- <link type="text/html" href="#{object.data["id"]}" rel="alternate" />
- <category term="2hu"/>
- <thr:in-reply-to ref="#{note_object.data["id"]}" href="someurl" />
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
- <link name="2hu" rel="emoji" href="corndog.png" />
- """
-
- tuple = ActivityRepresenter.to_simple_form(answer, user)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- assert clean(res) == clean(expected)
- end
-
- test "an announce activity" do
- note = insert(:note_activity)
- user = insert(:user)
- object = Object.normalize(note)
-
- {:ok, announce, _object} = ActivityPub.announce(user, object)
-
- announce = Activity.get_by_id(announce.id)
-
- note_user = User.get_cached_by_ap_id(note.data["actor"])
- note = Activity.get_by_id(note.id)
-
- note_xml =
- ActivityRepresenter.to_simple_form(note, note_user, true)
- |> :xmerl.export_simple_content(:xmerl_xml)
- |> to_string
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
- <id>#{announce.data["id"]}</id>
- <title>#{user.nickname} repeated a notice</title>
- <content type="html">RT #{object.data["content"]}</content>
- <published>#{announce.data["published"]}</published>
- <updated>#{announce.data["published"]}</updated>
- <ostatus:conversation ref="#{announce.data["context"]}">#{announce.data["context"]}</ostatus:conversation>
- <link ref="#{announce.data["context"]}" rel="ostatus:conversation" />
- <link rel="self" type="application/atom+xml" href="#{announce.data["id"]}"/>
- <activity:object>
- #{note_xml}
- </activity:object>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
- note.data["actor"]
- }"/>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
- """
-
- announce_xml =
- ActivityRepresenter.to_simple_form(announce, user)
- |> :xmerl.export_simple_content(:xmerl_xml)
- |> to_string
-
- assert clean(expected) == clean(announce_xml)
- end
-
- test "a like activity" do
- note = insert(:note)
- user = insert(:user)
- {:ok, like, _note} = ActivityPub.like(user, note)
-
- tuple = ActivityRepresenter.to_simple_form(like, user)
- refute is_nil(tuple)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- expected = """
- <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
- <id>#{like.data["id"]}</id>
- <title>New favorite by #{user.nickname}</title>
- <content type="html">#{user.nickname} favorited something</content>
- <published>#{like.data["published"]}</published>
- <updated>#{like.data["published"]}</updated>
- <activity:object>
- <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
- <id>#{note.data["id"]}</id>
- </activity:object>
- <ostatus:conversation ref="#{like.data["context"]}">#{like.data["context"]}</ostatus:conversation>
- <link ref="#{like.data["context"]}" rel="ostatus:conversation" />
- <link rel="self" type="application/atom+xml" href="#{like.data["id"]}"/>
- <thr:in-reply-to ref="#{note.data["id"]}" />
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
- note.data["actor"]
- }"/>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
- """
-
- assert clean(res) == clean(expected)
- end
-
- test "a follow activity" do
- follower = insert(:user)
- followed = insert(:user)
-
- {:ok, activity} =
- ActivityPub.insert(%{
- "type" => "Follow",
- "actor" => follower.ap_id,
- "object" => followed.ap_id,
- "to" => [followed.ap_id]
- })
-
- tuple = ActivityRepresenter.to_simple_form(activity, follower)
-
- refute is_nil(tuple)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/follow</activity:verb>
- <id>#{activity.data["id"]}</id>
- <title>#{follower.nickname} started following #{activity.data["object"]}</title>
- <content type="html"> #{follower.nickname} started following #{activity.data["object"]}</content>
- <published>#{activity.data["published"]}</published>
- <updated>#{activity.data["published"]}</updated>
- <activity:object>
- <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
- <id>#{activity.data["object"]}</id>
- <uri>#{activity.data["object"]}</uri>
- </activity:object>
- <link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
- activity.data["object"]
- }"/>
- """
-
- assert clean(res) == clean(expected)
- end
-
- test "an unfollow activity" do
- follower = insert(:user)
- followed = insert(:user)
- {:ok, _activity} = ActivityPub.follow(follower, followed)
- {:ok, activity} = ActivityPub.unfollow(follower, followed)
-
- tuple = ActivityRepresenter.to_simple_form(activity, follower)
-
- refute is_nil(tuple)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/unfollow</activity:verb>
- <id>#{activity.data["id"]}</id>
- <title>#{follower.nickname} stopped following #{followed.ap_id}</title>
- <content type="html"> #{follower.nickname} stopped following #{followed.ap_id}</content>
- <published>#{activity.data["published"]}</published>
- <updated>#{activity.data["published"]}</updated>
- <activity:object>
- <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
- <id>#{followed.ap_id}</id>
- <uri>#{followed.ap_id}</uri>
- </activity:object>
- <link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/>
- <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
- followed.ap_id
- }"/>
- """
-
- assert clean(res) == clean(expected)
- end
-
- test "a delete" do
- user = insert(:user)
-
- activity = %Activity{
- data: %{
- "id" => "ap_id",
- "type" => "Delete",
- "actor" => user.ap_id,
- "object" => "some_id",
- "published" => "2017-06-18T12:00:18+00:00"
- }
- }
-
- tuple = ActivityRepresenter.to_simple_form(activity, nil)
-
- refute is_nil(tuple)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
-
- expected = """
- <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
- <activity:verb>http://activitystrea.ms/schema/1.0/delete</activity:verb>
- <id>#{activity.data["object"]}</id>
- <title>An object was deleted</title>
- <content type="html">An object was deleted</content>
- <published>#{activity.data["published"]}</published>
- <updated>#{activity.data["published"]}</updated>
- """
-
- assert clean(res) == clean(expected)
- end
-
- test "an unknown activity" do
- tuple = ActivityRepresenter.to_simple_form(%Activity{}, nil)
- assert is_nil(tuple)
- end
-
- defp clean(string) do
- String.replace(string, ~r/\s/, "")
- end
-end
diff --git a/test/web/ostatus/feed_representer_test.exs b/test/web/ostatus/feed_representer_test.exs
deleted file mode 100644
index d1cadf1e4..000000000
--- a/test/web/ostatus/feed_representer_test.exs
+++ /dev/null
@@ -1,59 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OStatus.FeedRepresenterTest do
- use Pleroma.DataCase
- import Pleroma.Factory
- alias Pleroma.User
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.OStatus.ActivityRepresenter
- alias Pleroma.Web.OStatus.FeedRepresenter
- alias Pleroma.Web.OStatus.UserRepresenter
-
- test "returns a feed of the last 20 items of the user" do
- note_activity = insert(:note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
-
- tuple = FeedRepresenter.to_simple_form(user, [note_activity], [user])
-
- most_recent_update =
- note_activity.updated_at
- |> NaiveDateTime.to_iso8601()
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> to_string
-
- user_xml =
- UserRepresenter.to_simple_form(user)
- |> :xmerl.export_simple_content(:xmerl_xml)
-
- entry_xml =
- ActivityRepresenter.to_simple_form(note_activity, user)
- |> :xmerl.export_simple_content(:xmerl_xml)
-
- expected = """
- <feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0">
- <id>#{OStatus.feed_path(user)}</id>
- <title>#{user.nickname}'s timeline</title>
- <updated>#{most_recent_update}</updated>
- <logo>#{User.avatar_url(user)}</logo>
- <link rel="hub" href="#{OStatus.pubsub_path(user)}" />
- <link rel="salmon" href="#{OStatus.salmon_path(user)}" />
- <link rel="self" href="#{OStatus.feed_path(user)}" type="application/atom+xml" />
- <author>
- #{user_xml}
- </author>
- <link rel="next" href="#{OStatus.feed_path(user)}?max_id=#{note_activity.id}" type="application/atom+xml" />
- <entry>
- #{entry_xml}
- </entry>
- </feed>
- """
-
- assert clean(res) == clean(expected)
- end
-
- defp clean(string) do
- String.replace(string, ~r/\s/, "")
- end
-end
diff --git a/test/web/ostatus/incoming_documents/delete_handling_test.exs b/test/web/ostatus/incoming_documents/delete_handling_test.exs
deleted file mode 100644
index cd0447af7..000000000
--- a/test/web/ostatus/incoming_documents/delete_handling_test.exs
+++ /dev/null
@@ -1,48 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OStatus.DeleteHandlingTest do
- use Pleroma.DataCase
-
- import Pleroma.Factory
- import Tesla.Mock
-
- alias Pleroma.Activity
- alias Pleroma.Object
- alias Pleroma.Web.OStatus
-
- setup do
- mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- describe "deletions" do
- test "it removes the mentioned activity" do
- note = insert(:note_activity)
- second_note = insert(:note_activity)
- object = Object.normalize(note)
- second_object = Object.normalize(second_note)
- user = insert(:user)
-
- {:ok, like, _object} = Pleroma.Web.ActivityPub.ActivityPub.like(user, object)
-
- incoming =
- File.read!("test/fixtures/delete.xml")
- |> String.replace(
- "tag:mastodon.sdf.org,2017-06-10:objectId=310513:objectType=Status",
- object.data["id"]
- )
-
- {:ok, [delete]} = OStatus.handle_incoming(incoming)
-
- refute Activity.get_by_id(note.id)
- refute Activity.get_by_id(like.id)
- assert Object.get_by_ap_id(object.data["id"]).data["type"] == "Tombstone"
- assert Activity.get_by_id(second_note.id)
- assert Object.get_by_ap_id(second_object.data["id"])
-
- assert delete.data["type"] == "Delete"
- end
- end
-end
diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs
index b1af918d8..37b7b62f5 100644
--- a/test/web/ostatus/ostatus_controller_test.exs
+++ b/test/web/ostatus/ostatus_controller_test.exs
@@ -1,432 +1,331 @@
# 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.OStatusControllerTest do
use Pleroma.Web.ConnCase
- import ExUnit.CaptureLog
import Pleroma.Factory
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.OStatus.ActivityRepresenter
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
clear_config_all([:instance, :federating]) do
Pleroma.Config.put([:instance, :federating], true)
end
- describe "salmon_incoming" do
- test "decodes a salmon", %{conn: conn} do
- user = insert(:user)
- salmon = File.read!("test/fixtures/salmon.xml")
-
- assert capture_log(fn ->
- conn =
- conn
- |> put_req_header("content-type", "application/atom+xml")
- |> post("/users/#{user.nickname}/salmon", salmon)
-
- assert response(conn, 200)
- end) =~ "[error]"
- end
-
- test "decodes a salmon with a changed magic key", %{conn: conn} do
- user = insert(:user)
- salmon = File.read!("test/fixtures/salmon.xml")
-
- assert capture_log(fn ->
- conn =
- conn
- |> put_req_header("content-type", "application/atom+xml")
- |> post("/users/#{user.nickname}/salmon", salmon)
-
- assert response(conn, 200)
- end) =~ "[error]"
-
- # Wrong key
- info = %{
- magic_key:
- "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwrong1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB"
- }
-
- # Set a wrong magic-key for a user so it has to refetch
- "http://gs.example.org:4040/index.php/user/1"
- |> User.get_cached_by_ap_id()
- |> User.update_info(&User.Info.remote_user_creation(&1, info))
-
- assert capture_log(fn ->
- conn =
- build_conn()
- |> put_req_header("content-type", "application/atom+xml")
- |> post("/users/#{user.nickname}/salmon", salmon)
-
- assert response(conn, 200)
- end) =~ "[error]"
- end
- end
-
describe "GET object/2" do
- test "gets an object", %{conn: conn} do
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
- [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
- url = "/objects/#{uuid}"
-
- conn =
- conn
- |> put_req_header("accept", "application/xml")
- |> get(url)
-
- expected =
- ActivityRepresenter.to_simple_form(note_activity, user, true)
- |> ActivityRepresenter.wrap_with_entry()
- |> :xmerl.export_simple(:xmerl_xml)
- |> to_string
-
- assert response(conn, 200) == expected
- end
-
test "redirects to /notice/id for html format", %{conn: conn} do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
url = "/objects/#{uuid}"
conn =
conn
|> put_req_header("accept", "text/html")
|> get(url)
assert redirected_to(conn) == "/notice/#{note_activity.id}"
end
test "500s when user not found", %{conn: conn} do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
User.invalidate_cache(user)
Pleroma.Repo.delete(user)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
url = "/objects/#{uuid}"
conn =
conn
|> put_req_header("accept", "application/xml")
|> get(url)
assert response(conn, 500) == ~S({"error":"Something went wrong"})
end
test "404s on private objects", %{conn: conn} do
note_activity = insert(:direct_note_activity)
object = Object.normalize(note_activity)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
conn
|> get("/objects/#{uuid}")
|> response(404)
end
test "404s on nonexisting objects", %{conn: conn} do
conn
|> get("/objects/123")
|> response(404)
end
end
describe "GET activity/2" do
- test "gets an activity in xml format", %{conn: conn} do
- note_activity = insert(:note_activity)
- [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
-
- conn
- |> put_req_header("accept", "application/xml")
- |> get("/activities/#{uuid}")
- |> response(200)
- end
-
test "redirects to /notice/id for html format", %{conn: conn} do
note_activity = insert(:note_activity)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/activities/#{uuid}")
assert redirected_to(conn) == "/notice/#{note_activity.id}"
end
test "505s when user not found", %{conn: conn} do
note_activity = insert(:note_activity)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
user = User.get_cached_by_ap_id(note_activity.data["actor"])
User.invalidate_cache(user)
Pleroma.Repo.delete(user)
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/activities/#{uuid}")
assert response(conn, 500) == ~S({"error":"Something went wrong"})
end
- test "404s on deleted objects", %{conn: conn} do
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
-
- conn
- |> put_req_header("accept", "application/xml")
- |> get("/objects/#{uuid}")
- |> response(200)
-
- Object.delete(object)
-
- conn
- |> put_req_header("accept", "application/xml")
- |> get("/objects/#{uuid}")
- |> response(404)
- end
-
test "404s on private activities", %{conn: conn} do
note_activity = insert(:direct_note_activity)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
conn
|> get("/activities/#{uuid}")
|> response(404)
end
test "404s on nonexistent activities", %{conn: conn} do
conn
|> get("/activities/123")
|> response(404)
end
test "gets an activity in AS2 format", %{conn: conn} do
note_activity = insert(:note_activity)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
url = "/activities/#{uuid}"
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get(url)
assert json_response(conn, 200)
end
end
describe "GET notice/2" do
test "gets a notice in xml format", %{conn: conn} do
note_activity = insert(:note_activity)
conn
|> get("/notice/#{note_activity.id}")
|> response(200)
end
test "gets a notice in AS2 format", %{conn: conn} do
note_activity = insert(:note_activity)
conn
|> put_req_header("accept", "application/activity+json")
|> get("/notice/#{note_activity.id}")
|> json_response(200)
end
test "500s when actor not found", %{conn: conn} do
note_activity = insert(:note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
User.invalidate_cache(user)
Pleroma.Repo.delete(user)
conn =
conn
|> get("/notice/#{note_activity.id}")
assert response(conn, 500) == ~S({"error":"Something went wrong"})
end
test "only gets a notice in AS2 format for Create messages", %{conn: conn} do
note_activity = insert(:note_activity)
url = "/notice/#{note_activity.id}"
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get(url)
assert json_response(conn, 200)
user = insert(:user)
{:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user)
url = "/notice/#{like_activity.id}"
assert like_activity.data["type"] == "Like"
conn =
build_conn()
|> put_req_header("accept", "application/activity+json")
|> get(url)
assert response(conn, 404)
end
test "render html for redirect for html format", %{conn: conn} do
note_activity = insert(:note_activity)
resp =
conn
|> put_req_header("accept", "text/html")
|> get("/notice/#{note_activity.id}")
|> response(200)
assert resp =~
"<meta content=\"#{Pleroma.Web.base_url()}/notice/#{note_activity.id}\" property=\"og:url\">"
user = insert(:user)
{:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user)
assert like_activity.data["type"] == "Like"
resp =
conn
|> put_req_header("accept", "text/html")
|> get("/notice/#{like_activity.id}")
|> response(200)
assert resp =~ "<!--server-generated-meta-->"
end
test "404s a private notice", %{conn: conn} do
note_activity = insert(:direct_note_activity)
url = "/notice/#{note_activity.id}"
conn =
conn
|> get(url)
assert response(conn, 404)
end
test "404s a nonexisting notice", %{conn: conn} do
url = "/notice/123"
conn =
conn
|> get(url)
assert response(conn, 404)
end
end
describe "GET /notice/:id/embed_player" do
test "render embed player", %{conn: conn} do
note_activity = insert(:note_activity)
object = Pleroma.Object.normalize(note_activity)
object_data =
Map.put(object.data, "attachment", [
%{
"url" => [
%{
"href" =>
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
]
}
])
object
|> Ecto.Changeset.change(data: object_data)
|> Pleroma.Repo.update()
conn =
conn
|> get("/notice/#{note_activity.id}/embed_player")
assert Plug.Conn.get_resp_header(conn, "x-frame-options") == ["ALLOW"]
assert Plug.Conn.get_resp_header(
conn,
"content-security-policy"
) == [
"default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;"
]
assert response(conn, 200) =~
"<video controls loop><source src=\"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4\" type=\"video/mp4\">Your browser does not support video/mp4 playback.</video>"
end
test "404s when activity isn't create", %{conn: conn} do
note_activity = insert(:note_activity, data_attrs: %{"type" => "Like"})
assert conn
|> get("/notice/#{note_activity.id}/embed_player")
|> response(404)
end
test "404s when activity is direct message", %{conn: conn} do
note_activity = insert(:note_activity, data_attrs: %{"directMessage" => true})
assert conn
|> get("/notice/#{note_activity.id}/embed_player")
|> response(404)
end
test "404s when attachment is empty", %{conn: conn} do
note_activity = insert(:note_activity)
object = Pleroma.Object.normalize(note_activity)
object_data = Map.put(object.data, "attachment", [])
object
|> Ecto.Changeset.change(data: object_data)
|> Pleroma.Repo.update()
assert conn
|> get("/notice/#{note_activity.id}/embed_player")
|> response(404)
end
test "404s when attachment isn't audio or video", %{conn: conn} do
note_activity = insert(:note_activity)
object = Pleroma.Object.normalize(note_activity)
object_data =
Map.put(object.data, "attachment", [
%{
"url" => [
%{
"href" => "https://peertube.moe/static/webseed/480.jpg",
"mediaType" => "image/jpg",
"type" => "Link"
}
]
}
])
object
|> Ecto.Changeset.change(data: object_data)
|> Pleroma.Repo.update()
assert conn
|> get("/notice/#{note_activity.id}/embed_player")
|> response(404)
end
end
end
diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs
deleted file mode 100644
index 70a0e4473..000000000
--- a/test/web/ostatus/ostatus_test.exs
+++ /dev/null
@@ -1,645 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OStatusTest do
- use Pleroma.DataCase
- alias Pleroma.Activity
- alias Pleroma.Instances
- alias Pleroma.Object
- alias Pleroma.Repo
- alias Pleroma.User
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.XML
-
- import ExUnit.CaptureLog
- import Mock
- import Pleroma.Factory
-
- setup_all do
- Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- test "don't insert create notes twice" do
- incoming = File.read!("test/fixtures/incoming_note_activity.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- assert {:ok, [activity]} == OStatus.handle_incoming(incoming)
- end
-
- test "handle incoming note - GS, Salmon" do
- incoming = File.read!("test/fixtures/incoming_note_activity.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- user = User.get_cached_by_ap_id(activity.data["actor"])
- assert user.info.note_count == 1
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
-
- assert object.data["id"] == "tag:gs.example.org:4040,2017-04-23:noticeId=29:objectType=note"
-
- assert activity.data["published"] == "2017-04-23T14:51:03+00:00"
- assert object.data["published"] == "2017-04-23T14:51:03+00:00"
-
- assert activity.data["context"] ==
- "tag:gs.example.org:4040,2017-04-23:objectType=thread:nonce=f09e22f58abd5c7b"
-
- assert "http://pleroma.example.org:4000/users/lain3" in activity.data["to"]
- assert object.data["emoji"] == %{"marko" => "marko.png", "reimu" => "reimu.png"}
- assert activity.local == false
- end
-
- test "handle incoming notes - GS, subscription" do
- incoming = File.read!("test/fixtures/ostatus_incoming_post.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
- assert object.data["actor"] == "https://social.heldscal.la/user/23211"
- assert object.data["content"] == "Will it blend?"
- user = User.get_cached_by_ap_id(activity.data["actor"])
- assert User.ap_followers(user) in activity.data["to"]
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming notes with attachments - GS, subscription" do
- incoming = File.read!("test/fixtures/incoming_websub_gnusocial_attachments.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
- assert object.data["actor"] == "https://social.heldscal.la/user/23211"
- assert object.data["attachment"] |> length == 2
- assert object.data["external_url"] == "https://social.heldscal.la/notice/2020923"
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming notes with tags" do
- incoming = File.read!("test/fixtures/ostatus_incoming_post_tag.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert object.data["tag"] == ["nsfw"]
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming notes - Mastodon, salmon, reply" do
- # It uses the context of the replied to object
- Repo.insert!(%Object{
- data: %{
- "id" => "https://pleroma.soykaf.com/objects/c237d966-ac75-4fe3-a87a-d89d71a3a7a4",
- "context" => "2hu"
- }
- })
-
- incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
- assert object.data["actor"] == "https://mastodon.social/users/lambadalambda"
- assert activity.data["context"] == "2hu"
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming notes - Mastodon, with CW" do
- incoming = File.read!("test/fixtures/mastodon-note-cw.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
- assert object.data["actor"] == "https://mastodon.social/users/lambadalambda"
- assert object.data["summary"] == "technologic"
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming unlisted messages, put public into cc" do
- incoming = File.read!("test/fixtures/mastodon-note-unlisted.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- refute "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["cc"]
- refute "https://www.w3.org/ns/activitystreams#Public" in object.data["to"]
- assert "https://www.w3.org/ns/activitystreams#Public" in object.data["cc"]
- end
-
- test "handle incoming retweets - Mastodon, with CW" do
- incoming = File.read!("test/fixtures/cw_retweet.xml")
- {:ok, [[_activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
- retweeted_object = Object.normalize(retweeted_activity)
-
- assert retweeted_object.data["summary"] == "Hey."
- end
-
- test "handle incoming notes - GS, subscription, reply" do
- incoming = File.read!("test/fixtures/ostatus_incoming_reply.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
- assert object.data["actor"] == "https://social.heldscal.la/user/23211"
-
- assert object.data["content"] ==
- "@<a href=\"https://gs.archae.me/user/4687\" class=\"h-card u-url p-nickname mention\" title=\"shpbot\">shpbot</a> why not indeed."
-
- assert object.data["inReplyTo"] ==
- "tag:gs.archae.me,2017-04-30:noticeId=778260:objectType=note"
-
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
- end
-
- test "handle incoming retweets - GS, subscription" do
- incoming = File.read!("test/fixtures/share-gs.xml")
- {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["type"] == "Announce"
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- assert activity.data["object"] == retweeted_activity.data["object"]
- assert "https://pleroma.soykaf.com/users/lain" in activity.data["to"]
- refute activity.local
-
- retweeted_activity = Activity.get_by_id(retweeted_activity.id)
- retweeted_object = Object.normalize(retweeted_activity)
- assert retweeted_activity.data["type"] == "Create"
- assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain"
- refute retweeted_activity.local
- assert retweeted_object.data["announcement_count"] == 1
- assert String.contains?(retweeted_object.data["content"], "mastodon")
- refute String.contains?(retweeted_object.data["content"], "Test account")
- end
-
- test "handle incoming retweets - GS, subscription - local message" do
- incoming = File.read!("test/fixtures/share-gs-local.xml")
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
- user = User.get_cached_by_ap_id(note_activity.data["actor"])
-
- incoming =
- incoming
- |> String.replace("LOCAL_ID", object.data["id"])
- |> String.replace("LOCAL_USER", user.ap_id)
-
- {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["type"] == "Announce"
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- assert activity.data["object"] == object.data["id"]
- assert user.ap_id in activity.data["to"]
- refute activity.local
-
- retweeted_activity = Activity.get_by_id(retweeted_activity.id)
- assert note_activity.id == retweeted_activity.id
- assert retweeted_activity.data["type"] == "Create"
- assert retweeted_activity.data["actor"] == user.ap_id
- assert retweeted_activity.local
- assert Object.normalize(retweeted_activity).data["announcement_count"] == 1
- end
-
- test "handle incoming retweets - Mastodon, salmon" do
- incoming = File.read!("test/fixtures/share.xml")
- {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
- retweeted_object = Object.normalize(retweeted_activity)
-
- assert activity.data["type"] == "Announce"
- assert activity.data["actor"] == "https://mastodon.social/users/lambadalambda"
- assert activity.data["object"] == retweeted_activity.data["object"]
-
- assert activity.data["id"] ==
- "tag:mastodon.social,2017-05-03:objectId=4934452:objectType=Status"
-
- refute activity.local
- assert retweeted_activity.data["type"] == "Create"
- assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain"
- refute retweeted_activity.local
- refute String.contains?(retweeted_object.data["content"], "Test account")
- end
-
- test "handle incoming favorites - GS, websub" do
- capture_log(fn ->
- incoming = File.read!("test/fixtures/favorite.xml")
- {:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["type"] == "Like"
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- assert activity.data["object"] == favorited_activity.data["object"]
-
- assert activity.data["id"] ==
- "tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061643:2017-05-05T09:12:50+00:00"
-
- refute activity.local
- assert favorited_activity.data["type"] == "Create"
- assert favorited_activity.data["actor"] == "https://shitposter.club/user/1"
-
- assert favorited_activity.data["object"] ==
- "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
-
- refute favorited_activity.local
- end)
- end
-
- test "handle conversation references" do
- incoming = File.read!("test/fixtures/mastodon_conversation.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["context"] ==
- "tag:mastodon.social,2017-08-28:objectId=7876885:objectType=Conversation"
- end
-
- test "handle incoming favorites with locally available object - GS, websub" do
- note_activity = insert(:note_activity)
- object = Object.normalize(note_activity)
-
- incoming =
- File.read!("test/fixtures/favorite_with_local_note.xml")
- |> String.replace("localid", object.data["id"])
-
- {:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["type"] == "Like"
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- assert activity.data["object"] == object.data["id"]
- refute activity.local
- assert note_activity.id == favorited_activity.id
- assert favorited_activity.local
- end
-
- test_with_mock "handle incoming replies, fetching replied-to activities if we don't have them",
- OStatus,
- [:passthrough],
- [] do
- incoming = File.read!("test/fixtures/incoming_note_activity_answer.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity, false)
-
- assert activity.data["type"] == "Create"
- assert object.data["type"] == "Note"
-
- assert object.data["inReplyTo"] ==
- "http://pleroma.example.org:4000/objects/55bce8fc-b423-46b1-af71-3759ab4670bc"
-
- assert "http://pleroma.example.org:4000/users/lain5" in activity.data["to"]
-
- assert object.data["id"] == "tag:gs.example.org:4040,2017-04-25:noticeId=55:objectType=note"
-
- assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
-
- assert called(OStatus.fetch_activity_from_url(object.data["inReplyTo"], :_))
- end
-
- test_with_mock "handle incoming replies, not fetching replied-to activities beyond max_replies_depth",
- OStatus,
- [:passthrough],
- [] do
- incoming = File.read!("test/fixtures/incoming_note_activity_answer.xml")
-
- with_mock Pleroma.Web.Federator,
- allowed_incoming_reply_depth?: fn _ -> false end do
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- object = Object.normalize(activity, false)
-
- refute called(OStatus.fetch_activity_from_url(object.data["inReplyTo"], :_))
- end
- end
-
- test "handle incoming follows" do
- incoming = File.read!("test/fixtures/follow.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
- assert activity.data["type"] == "Follow"
-
- assert activity.data["id"] ==
- "tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00"
-
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- assert activity.data["object"] == "https://pawoo.net/users/pekorino"
- refute activity.local
-
- follower = User.get_cached_by_ap_id(activity.data["actor"])
- followed = User.get_cached_by_ap_id(activity.data["object"])
-
- assert User.following?(follower, followed)
- end
-
- test "refuse following over OStatus if the followed's account is locked" do
- incoming = File.read!("test/fixtures/follow.xml")
- _user = insert(:user, info: %{locked: true}, ap_id: "https://pawoo.net/users/pekorino")
-
- {:ok, [{:error, "It's not possible to follow locked accounts over OStatus"}]} =
- OStatus.handle_incoming(incoming)
- end
-
- test "handle incoming unfollows with existing follow" do
- incoming_follow = File.read!("test/fixtures/follow.xml")
- {:ok, [_activity]} = OStatus.handle_incoming(incoming_follow)
-
- incoming = File.read!("test/fixtures/unfollow.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["type"] == "Undo"
-
- assert activity.data["id"] ==
- "undo:tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00"
-
- assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
- embedded_object = activity.data["object"]
- assert is_map(embedded_object)
- assert embedded_object["type"] == "Follow"
- assert embedded_object["object"] == "https://pawoo.net/users/pekorino"
- refute activity.local
-
- follower = User.get_cached_by_ap_id(activity.data["actor"])
- followed = User.get_cached_by_ap_id(embedded_object["object"])
-
- refute User.following?(follower, followed)
- end
-
- test "it clears `unreachable` federation status of the sender" do
- incoming_reaction_xml = File.read!("test/fixtures/share-gs.xml")
- doc = XML.parse_document(incoming_reaction_xml)
- actor_uri = XML.string_from_xpath("//author/uri[1]", doc)
- reacted_to_author_uri = XML.string_from_xpath("//author/uri[2]", doc)
-
- Instances.set_consistently_unreachable(actor_uri)
- Instances.set_consistently_unreachable(reacted_to_author_uri)
- refute Instances.reachable?(actor_uri)
- refute Instances.reachable?(reacted_to_author_uri)
-
- {:ok, _} = OStatus.handle_incoming(incoming_reaction_xml)
- assert Instances.reachable?(actor_uri)
- refute Instances.reachable?(reacted_to_author_uri)
- end
-
- describe "new remote user creation" do
- test "returns local users" do
- local_user = insert(:user)
- {:ok, user} = OStatus.find_or_make_user(local_user.ap_id)
-
- assert user == local_user
- end
-
- test "tries to use the information in poco fields" do
- uri = "https://social.heldscal.la/user/23211"
-
- {:ok, user} = OStatus.find_or_make_user(uri)
-
- user = User.get_cached_by_id(user.id)
- assert user.name == "Constance Variable"
- assert user.nickname == "lambadalambda@social.heldscal.la"
- assert user.local == false
- assert user.info.uri == uri
- assert user.ap_id == uri
- assert user.bio == "Call me Deacon Blues."
- assert user.avatar["type"] == "Image"
-
- {:ok, user_again} = OStatus.find_or_make_user(uri)
-
- assert user == user_again
- end
-
- test "find_or_make_user sets all the nessary input fields" do
- uri = "https://social.heldscal.la/user/23211"
- {:ok, user} = OStatus.find_or_make_user(uri)
-
- assert user.info ==
- %User.Info{
- id: user.info.id,
- ap_enabled: false,
- background: %{},
- banner: %{},
- blocks: [],
- deactivated: false,
- default_scope: "public",
- domain_blocks: [],
- follower_count: 0,
- is_admin: false,
- is_moderator: false,
- keys: nil,
- locked: false,
- no_rich_text: false,
- note_count: 0,
- settings: nil,
- source_data: %{},
- hub: "https://social.heldscal.la/main/push/hub",
- magic_key:
- "RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB",
- salmon: "https://social.heldscal.la/main/salmon/user/23211",
- topic: "https://social.heldscal.la/api/statuses/user_timeline/23211.atom",
- uri: "https://social.heldscal.la/user/23211"
- }
- end
-
- test "find_make_or_update_actor takes an author element and returns an updated user" do
- uri = "https://social.heldscal.la/user/23211"
-
- {:ok, user} = OStatus.find_or_make_user(uri)
- old_name = user.name
- old_bio = user.bio
- change = Ecto.Changeset.change(user, %{avatar: nil, bio: nil, name: nil})
-
- {:ok, user} = Repo.update(change)
- refute user.avatar
-
- doc = XML.parse_document(File.read!("test/fixtures/23211.atom"))
- [author] = :xmerl_xpath.string('//author[1]', doc)
- {:ok, user} = OStatus.find_make_or_update_actor(author)
- assert user.avatar["type"] == "Image"
- assert user.name == old_name
- assert user.bio == old_bio
-
- {:ok, user_again} = OStatus.find_make_or_update_actor(author)
- assert user_again == user
- end
-
- test "find_or_make_user disallows protocol downgrade" do
- user = insert(:user, %{local: true})
- {:ok, user} = OStatus.find_or_make_user(user.ap_id)
-
- assert User.ap_enabled?(user)
-
- user =
- insert(:user, %{
- ap_id: "https://social.heldscal.la/user/23211",
- info: %{ap_enabled: true},
- local: false
- })
-
- assert User.ap_enabled?(user)
-
- {:ok, user} = OStatus.find_or_make_user(user.ap_id)
- assert User.ap_enabled?(user)
- end
-
- test "find_make_or_update_actor disallows protocol downgrade" do
- user = insert(:user, %{local: true})
- {:ok, user} = OStatus.find_or_make_user(user.ap_id)
-
- assert User.ap_enabled?(user)
-
- user =
- insert(:user, %{
- ap_id: "https://social.heldscal.la/user/23211",
- info: %{ap_enabled: true},
- local: false
- })
-
- assert User.ap_enabled?(user)
-
- {:ok, user} = OStatus.find_or_make_user(user.ap_id)
- assert User.ap_enabled?(user)
-
- doc = XML.parse_document(File.read!("test/fixtures/23211.atom"))
- [author] = :xmerl_xpath.string('//author[1]', doc)
- {:error, :invalid_protocol} = OStatus.find_make_or_update_actor(author)
- end
- end
-
- describe "gathering user info from a user id" do
- test "it returns user info in a hash" do
- user = "shp@social.heldscal.la"
-
- # TODO: make test local
- {:ok, data} = OStatus.gather_user_info(user)
-
- expected = %{
- "hub" => "https://social.heldscal.la/main/push/hub",
- "magic_key" =>
- "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB",
- "name" => "shp",
- "nickname" => "shp",
- "salmon" => "https://social.heldscal.la/main/salmon/user/29191",
- "subject" => "acct:shp@social.heldscal.la",
- "topic" => "https://social.heldscal.la/api/statuses/user_timeline/29191.atom",
- "uri" => "https://social.heldscal.la/user/29191",
- "host" => "social.heldscal.la",
- "fqn" => user,
- "bio" => "cofe",
- "avatar" => %{
- "type" => "Image",
- "url" => [
- %{
- "href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg",
- "mediaType" => "image/jpeg",
- "type" => "Link"
- }
- ]
- },
- "subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}",
- "ap_id" => nil
- }
-
- assert data == expected
- end
-
- test "it works with the uri" do
- user = "https://social.heldscal.la/user/29191"
-
- # TODO: make test local
- {:ok, data} = OStatus.gather_user_info(user)
-
- expected = %{
- "hub" => "https://social.heldscal.la/main/push/hub",
- "magic_key" =>
- "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB",
- "name" => "shp",
- "nickname" => "shp",
- "salmon" => "https://social.heldscal.la/main/salmon/user/29191",
- "subject" => "https://social.heldscal.la/user/29191",
- "topic" => "https://social.heldscal.la/api/statuses/user_timeline/29191.atom",
- "uri" => "https://social.heldscal.la/user/29191",
- "host" => "social.heldscal.la",
- "fqn" => user,
- "bio" => "cofe",
- "avatar" => %{
- "type" => "Image",
- "url" => [
- %{
- "href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg",
- "mediaType" => "image/jpeg",
- "type" => "Link"
- }
- ]
- },
- "subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}",
- "ap_id" => nil
- }
-
- assert data == expected
- end
- end
-
- describe "fetching a status by it's HTML url" do
- test "it builds a missing status from an html url" do
- capture_log(fn ->
- url = "https://shitposter.club/notice/2827873"
- {:ok, [activity]} = OStatus.fetch_activity_from_url(url)
-
- assert activity.data["actor"] == "https://shitposter.club/user/1"
-
- assert activity.data["object"] ==
- "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
- end)
- end
-
- test "it works for atom notes, too" do
- url = "https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056"
- {:ok, [activity]} = OStatus.fetch_activity_from_url(url)
- assert activity.data["actor"] == "https://social.sakamoto.gq/users/eal"
- assert activity.data["object"] == url
- end
- end
-
- test "it doesn't add nil in the to field" do
- incoming = File.read!("test/fixtures/nil_mention_entry.xml")
- {:ok, [activity]} = OStatus.handle_incoming(incoming)
-
- assert activity.data["to"] == [
- "http://localhost:4001/users/atarifrosch@social.stopwatchingus-heidelberg.de/followers",
- "https://www.w3.org/ns/activitystreams#Public"
- ]
- end
-
- describe "is_representable?" do
- test "Note objects are representable" do
- note_activity = insert(:note_activity)
-
- assert OStatus.is_representable?(note_activity)
- end
-
- test "Article objects are not representable" do
- note_activity = insert(:note_activity)
- note_object = Object.normalize(note_activity)
-
- note_data =
- note_object.data
- |> Map.put("type", "Article")
-
- Cachex.clear(:object_cache)
-
- cs = Object.change(note_object, %{data: note_data})
- {:ok, _article_object} = Repo.update(cs)
-
- # the underlying object is now an Article instead of a note, so this should fail
- refute OStatus.is_representable?(note_activity)
- end
- end
-
- describe "make_user/2" do
- test "creates new user" do
- {:ok, user} = OStatus.make_user("https://social.heldscal.la/user/23211")
-
- created_user =
- User
- |> Repo.get_by(ap_id: "https://social.heldscal.la/user/23211")
- |> Map.put(:last_digest_emailed_at, nil)
-
- assert user.info
- assert user == created_user
- end
- end
-end
diff --git a/test/web/ostatus/user_representer_test.exs b/test/web/ostatus/user_representer_test.exs
deleted file mode 100644
index e3863d2e9..000000000
--- a/test/web/ostatus/user_representer_test.exs
+++ /dev/null
@@ -1,38 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.OStatus.UserRepresenterTest do
- use Pleroma.DataCase
- alias Pleroma.Web.OStatus.UserRepresenter
-
- import Pleroma.Factory
- alias Pleroma.User
-
- test "returns a user with id, uri, name and link" do
- user = insert(:user, %{nickname: "レイン"})
- tuple = UserRepresenter.to_simple_form(user)
-
- res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> to_string
-
- expected = """
- <id>#{user.ap_id}</id>
- <activity:object>http://activitystrea.ms/schema/1.0/person</activity:object>
- <uri>#{user.ap_id}</uri>
- <poco:preferredUsername>#{user.nickname}</poco:preferredUsername>
- <poco:displayName>#{user.name}</poco:displayName>
- <poco:note>#{user.bio}</poco:note>
- <summary>#{user.bio}</summary>
- <name>#{user.nickname}</name>
- <link rel="avatar" href="#{User.avatar_url(user)}" />
- <link rel="header" href="#{User.banner_url(user)}" />
- <ap_enabled>true</ap_enabled>
- """
-
- assert clean(res) == clean(expected)
- end
-
- defp clean(string) do
- String.replace(string, ~r/\s/, "")
- end
-end
diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs
index 3b4665afd..c809f510f 100644
--- a/test/web/pleroma_api/controllers/account_controller_test.exs
+++ b/test/web/pleroma_api/controllers/account_controller_test.exs
@@ -1,395 +1,394 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Config
- alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
import Swoosh.TestAssertions
@image "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"
describe "POST /api/v1/pleroma/accounts/confirmation_resend" do
setup do
{:ok, user} =
insert(:user)
- |> User.change_info(&User.Info.confirmation_changeset(&1, need_confirmation: true))
- |> Repo.update()
+ |> User.confirmation_changeset(need_confirmation: true)
+ |> User.update_and_set_cache()
- assert user.info.confirmation_pending
+ assert user.confirmation_pending
[user: user]
end
clear_config([:instance, :account_activation_required]) do
Config.put([:instance, :account_activation_required], true)
end
test "resend account confirmation email", %{conn: conn, user: user} do
conn
|> assign(:user, user)
|> post("/api/v1/pleroma/accounts/confirmation_resend?email=#{user.email}")
|> json_response(:no_content)
ObanHelpers.perform_all()
email = Pleroma.Emails.UserEmail.account_confirmation_email(user)
notify_email = Config.get([:instance, :notify_email])
instance_name = Config.get([:instance, :name])
assert_email_sent(
from: {instance_name, notify_email},
to: {user.name, user.email},
html_body: email.html_body
)
end
end
describe "PATCH /api/v1/pleroma/accounts/update_avatar" do
test "user avatar can be set", %{conn: conn} do
user = insert(:user)
avatar_image = File.read!("test/fixtures/avatar_data_uri")
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/pleroma/accounts/update_avatar", %{img: avatar_image})
user = refresh_record(user)
assert %{
"name" => _,
"type" => _,
"url" => [
%{
"href" => _,
"mediaType" => _,
"type" => _
}
]
} = user.avatar
assert %{"url" => _} = json_response(conn, 200)
end
test "user avatar can be reset", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/pleroma/accounts/update_avatar", %{img: ""})
user = User.get_cached_by_id(user.id)
assert user.avatar == nil
assert %{"url" => nil} = json_response(conn, 200)
end
end
describe "PATCH /api/v1/pleroma/accounts/update_banner" do
test "can set profile banner", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => @image})
user = refresh_record(user)
- assert user.info.banner["type"] == "Image"
+ assert user.banner["type"] == "Image"
assert %{"url" => _} = json_response(conn, 200)
end
test "can reset profile banner", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => ""})
user = refresh_record(user)
- assert user.info.banner == %{}
+ assert user.banner == %{}
assert %{"url" => nil} = json_response(conn, 200)
end
end
describe "PATCH /api/v1/pleroma/accounts/update_background" do
test "background image can be set", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/pleroma/accounts/update_background", %{"img" => @image})
user = refresh_record(user)
- assert user.info.background["type"] == "Image"
+ assert user.background["type"] == "Image"
assert %{"url" => _} = json_response(conn, 200)
end
test "background image can be reset", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/pleroma/accounts/update_background", %{"img" => ""})
user = refresh_record(user)
- assert user.info.background == %{}
+ assert user.background == %{}
assert %{"url" => nil} = json_response(conn, 200)
end
end
describe "getting favorites timeline of specified user" do
setup do
- [current_user, user] = insert_pair(:user, %{info: %{hide_favorites: false}})
+ [current_user, user] = insert_pair(:user, hide_favorites: false)
[current_user: current_user, user: user]
end
test "returns list of statuses favorited by specified user", %{
conn: conn,
current_user: current_user,
user: user
} do
[activity | _] = insert_pair(:note_activity)
CommonAPI.favorite(activity.id, user)
response =
conn
|> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok)
[like] = response
assert length(response) == 1
assert like["id"] == activity.id
end
test "returns favorites for specified user_id when user is not logged in", %{
conn: conn,
user: user
} do
activity = insert(:note_activity)
CommonAPI.favorite(activity.id, user)
response =
conn
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok)
assert length(response) == 1
end
test "returns favorited DM only when user is logged in and he is one of recipients", %{
conn: conn,
current_user: current_user,
user: user
} do
{:ok, direct} =
CommonAPI.post(current_user, %{
"status" => "Hi @#{user.nickname}!",
"visibility" => "direct"
})
CommonAPI.favorite(direct.id, user)
response =
conn
|> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok)
assert length(response) == 1
anonymous_response =
conn
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok)
assert Enum.empty?(anonymous_response)
end
test "does not return others' favorited DM when user is not one of recipients", %{
conn: conn,
current_user: current_user,
user: user
} do
user_two = insert(:user)
{:ok, direct} =
CommonAPI.post(user_two, %{
"status" => "Hi @#{user.nickname}!",
"visibility" => "direct"
})
CommonAPI.favorite(direct.id, user)
response =
conn
|> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "paginates favorites using since_id and max_id", %{
conn: conn,
current_user: current_user,
user: user
} do
activities = insert_list(10, :note_activity)
Enum.each(activities, fn activity ->
CommonAPI.favorite(activity.id, user)
end)
third_activity = Enum.at(activities, 2)
seventh_activity = Enum.at(activities, 6)
response =
conn
|> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{
since_id: third_activity.id,
max_id: seventh_activity.id
})
|> json_response(:ok)
assert length(response) == 3
refute third_activity in response
refute seventh_activity in response
end
test "limits favorites using limit parameter", %{
conn: conn,
current_user: current_user,
user: user
} do
7
|> insert_list(:note_activity)
|> Enum.each(fn activity ->
CommonAPI.favorite(activity.id, user)
end)
response =
conn
|> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{limit: "3"})
|> json_response(:ok)
assert length(response) == 3
end
test "returns empty response when user does not have any favorited statuses", %{
conn: conn,
current_user: current_user,
user: user
} do
response =
conn
|> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok)
assert Enum.empty?(response)
end
test "returns 404 error when specified user is not exist", %{conn: conn} do
conn = get(conn, "/api/v1/pleroma/accounts/test/favourites")
assert json_response(conn, 404) == %{"error" => "Record not found"}
end
test "returns 403 error when user has hidden own favorites", %{
conn: conn,
current_user: current_user
} do
- user = insert(:user, %{info: %{hide_favorites: true}})
+ user = insert(:user, hide_favorites: true)
activity = insert(:note_activity)
CommonAPI.favorite(activity.id, user)
conn =
conn
|> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
end
test "hides favorites for new users by default", %{conn: conn, current_user: current_user} do
user = insert(:user)
activity = insert(:note_activity)
CommonAPI.favorite(activity.id, user)
conn =
conn
|> assign(:user, current_user)
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
- assert user.info.hide_favorites
+ assert user.hide_favorites
assert json_response(conn, 403) == %{"error" => "Can't get favorites"}
end
end
describe "subscribing / unsubscribing" do
test "subscribing / unsubscribing to a user", %{conn: conn} do
user = insert(:user)
subscription_target = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe")
assert %{"id" => _id, "subscribing" => true} = json_response(conn, 200)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe")
assert %{"id" => _id, "subscribing" => false} = json_response(conn, 200)
end
end
describe "subscribing" do
test "returns 404 when subscription_target not found", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/pleroma/accounts/target_id/subscribe")
assert %{"error" => "Record not found"} = json_response(conn, 404)
end
end
describe "unsubscribing" do
test "returns 404 when subscription_target not found", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/pleroma/accounts/target_id/unsubscribe")
assert %{"error" => "Record not found"} = json_response(conn, 404)
end
end
end
diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
index 5f74460e8..3d3becefd 100644
--- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
@@ -1,463 +1,463 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
use Pleroma.Web.ConnCase
import Tesla.Mock
import Pleroma.Factory
@emoji_dir_path Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)
test "shared & non-shared pack information in list_packs is ok" do
conn = build_conn()
resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
assert Map.has_key?(resp, "test_pack")
pack = resp["test_pack"]
assert Map.has_key?(pack["pack"], "download-sha256")
assert pack["pack"]["can-download"]
assert pack["files"] == %{"blank" => "blank.png"}
# Non-shared pack
assert Map.has_key?(resp, "test_pack_nonshared")
pack = resp["test_pack_nonshared"]
refute pack["pack"]["shared"]
refute pack["pack"]["can-download"]
end
test "listing remote packs" do
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
conn = build_conn() |> assign(:user, admin)
resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
mock(fn
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})
%{method: :get, url: "https://example.com/api/pleroma/emoji/packs"} ->
json(resp)
end)
assert conn
|> post(emoji_api_path(conn, :list_from), %{instance_address: "https://example.com"})
|> json_response(200) == resp
end
test "downloading a shared pack from download_shared" do
conn = build_conn()
resp =
conn
|> get(emoji_api_path(conn, :download_shared, "test_pack"))
|> response(200)
{:ok, arch} = :zip.unzip(resp, [:memory])
assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end)
assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
end
test "downloading shared & unshared packs from another instance via download_from, deleting them" do
on_exit(fn ->
File.rm_rf!("#{@emoji_dir_path}/test_pack2")
File.rm_rf!("#{@emoji_dir_path}/test_pack_nonshared2")
end)
mock(fn
%{method: :get, url: "https://old-instance/.well-known/nodeinfo"} ->
json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: []}})
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})
%{
method: :get,
url: "https://example.com/api/pleroma/emoji/packs/list"
} ->
conn = build_conn()
conn
|> get(emoji_api_path(conn, :list_packs))
|> json_response(200)
|> json()
%{
method: :get,
url: "https://example.com/api/pleroma/emoji/packs/download_shared/test_pack"
} ->
conn = build_conn()
conn
|> get(emoji_api_path(conn, :download_shared, "test_pack"))
|> response(200)
|> text()
%{
method: :get,
url: "https://nonshared-pack"
} ->
text(File.read!("#{@emoji_dir_path}/test_pack_nonshared/nonshared.zip"))
end)
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
conn = build_conn() |> assign(:user, admin)
assert (conn
|> put_req_header("content-type", "application/json")
|> post(
emoji_api_path(
conn,
:download_from
),
%{
instance_address: "https://old-instance",
pack_name: "test_pack",
as: "test_pack2"
}
|> Jason.encode!()
)
|> json_response(500))["error"] =~ "does not support"
assert conn
|> put_req_header("content-type", "application/json")
|> post(
emoji_api_path(
conn,
:download_from
),
%{
instance_address: "https://example.com",
pack_name: "test_pack",
as: "test_pack2"
}
|> Jason.encode!()
)
|> json_response(200) == "ok"
assert File.exists?("#{@emoji_dir_path}/test_pack2/pack.json")
assert File.exists?("#{@emoji_dir_path}/test_pack2/blank.png")
assert conn
|> delete(emoji_api_path(conn, :delete, "test_pack2"))
|> json_response(200) == "ok"
refute File.exists?("#{@emoji_dir_path}/test_pack2")
# non-shared, downloaded from the fallback URL
conn = build_conn() |> assign(:user, admin)
assert conn
|> put_req_header("content-type", "application/json")
|> post(
emoji_api_path(
conn,
:download_from
),
%{
instance_address: "https://example.com",
pack_name: "test_pack_nonshared",
as: "test_pack_nonshared2"
}
|> Jason.encode!()
)
|> json_response(200) == "ok"
assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/pack.json")
assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/blank.png")
assert conn
|> delete(emoji_api_path(conn, :delete, "test_pack_nonshared2"))
|> json_response(200) == "ok"
refute File.exists?("#{@emoji_dir_path}/test_pack_nonshared2")
end
describe "updating pack metadata" do
setup do
pack_file = "#{@emoji_dir_path}/test_pack/pack.json"
original_content = File.read!(pack_file)
on_exit(fn ->
File.write!(pack_file, original_content)
end)
{:ok,
- admin: insert(:user, info: %{is_admin: true}),
+ admin: insert(:user, is_admin: true),
pack_file: pack_file,
new_data: %{
"license" => "Test license changed",
"homepage" => "https://pleroma.social",
"description" => "Test description",
"share-files" => false
}}
end
test "for a pack without a fallback source", ctx do
conn = build_conn()
assert conn
|> assign(:user, ctx[:admin])
|> post(
emoji_api_path(conn, :update_metadata, "test_pack"),
%{
"new_data" => ctx[:new_data]
}
)
|> json_response(200) == ctx[:new_data]
assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data]
end
test "for a pack with a fallback source", ctx do
mock(fn
%{
method: :get,
url: "https://nonshared-pack"
} ->
text(File.read!("#{@emoji_dir_path}/test_pack_nonshared/nonshared.zip"))
end)
new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
new_data_with_sha =
Map.put(
new_data,
"fallback-src-sha256",
"74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF"
)
conn = build_conn()
assert conn
|> assign(:user, ctx[:admin])
|> post(
emoji_api_path(conn, :update_metadata, "test_pack"),
%{
"new_data" => new_data
}
)
|> json_response(200) == new_data_with_sha
assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha
end
test "when the fallback source doesn't have all the files", ctx do
mock(fn
%{
method: :get,
url: "https://nonshared-pack"
} ->
{:ok, {'empty.zip', empty_arch}} = :zip.zip('empty.zip', [], [:memory])
text(empty_arch)
end)
new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
conn = build_conn()
assert (conn
|> assign(:user, ctx[:admin])
|> post(
emoji_api_path(conn, :update_metadata, "test_pack"),
%{
"new_data" => new_data
}
)
|> json_response(:bad_request))["error"] =~ "does not have all"
end
end
test "updating pack files" do
pack_file = "#{@emoji_dir_path}/test_pack/pack.json"
original_content = File.read!(pack_file)
on_exit(fn ->
File.write!(pack_file, original_content)
File.rm_rf!("#{@emoji_dir_path}/test_pack/blank_url.png")
File.rm_rf!("#{@emoji_dir_path}/test_pack/dir")
File.rm_rf!("#{@emoji_dir_path}/test_pack/dir_2")
end)
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
conn = build_conn()
same_name = %{
"action" => "add",
"shortcode" => "blank",
"filename" => "dir/blank.png",
"file" => %Plug.Upload{
filename: "blank.png",
path: "#{@emoji_dir_path}/test_pack/blank.png"
}
}
different_name = %{same_name | "shortcode" => "blank_2"}
conn = conn |> assign(:user, admin)
assert (conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), same_name)
|> json_response(:conflict))["error"] =~ "already exists"
assert conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), different_name)
|> json_response(200) == %{"blank" => "blank.png", "blank_2" => "dir/blank.png"}
assert File.exists?("#{@emoji_dir_path}/test_pack/dir/blank.png")
assert conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), %{
"action" => "update",
"shortcode" => "blank_2",
"new_shortcode" => "blank_3",
"new_filename" => "dir_2/blank_3.png"
})
|> json_response(200) == %{"blank" => "blank.png", "blank_3" => "dir_2/blank_3.png"}
refute File.exists?("#{@emoji_dir_path}/test_pack/dir/")
assert File.exists?("#{@emoji_dir_path}/test_pack/dir_2/blank_3.png")
assert conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), %{
"action" => "remove",
"shortcode" => "blank_3"
})
|> json_response(200) == %{"blank" => "blank.png"}
refute File.exists?("#{@emoji_dir_path}/test_pack/dir_2/")
mock(fn
%{
method: :get,
url: "https://test-blank/blank_url.png"
} ->
text(File.read!("#{@emoji_dir_path}/test_pack/blank.png"))
end)
# The name should be inferred from the URL ending
from_url = %{
"action" => "add",
"shortcode" => "blank_url",
"file" => "https://test-blank/blank_url.png"
}
assert conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), from_url)
|> json_response(200) == %{
"blank" => "blank.png",
"blank_url" => "blank_url.png"
}
assert File.exists?("#{@emoji_dir_path}/test_pack/blank_url.png")
assert conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), %{
"action" => "remove",
"shortcode" => "blank_url"
})
|> json_response(200) == %{"blank" => "blank.png"}
refute File.exists?("#{@emoji_dir_path}/test_pack/blank_url.png")
end
test "creating and deleting a pack" do
on_exit(fn ->
File.rm_rf!("#{@emoji_dir_path}/test_created")
end)
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
conn = build_conn() |> assign(:user, admin)
assert conn
|> put_req_header("content-type", "application/json")
|> put(
emoji_api_path(
conn,
:create,
"test_created"
)
)
|> json_response(200) == "ok"
assert File.exists?("#{@emoji_dir_path}/test_created/pack.json")
assert Jason.decode!(File.read!("#{@emoji_dir_path}/test_created/pack.json")) == %{
"pack" => %{},
"files" => %{}
}
assert conn
|> delete(emoji_api_path(conn, :delete, "test_created"))
|> json_response(200) == "ok"
refute File.exists?("#{@emoji_dir_path}/test_created/pack.json")
end
test "filesystem import" do
on_exit(fn ->
File.rm!("#{@emoji_dir_path}/test_pack_for_import/emoji.txt")
File.rm!("#{@emoji_dir_path}/test_pack_for_import/pack.json")
end)
conn = build_conn()
resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
refute Map.has_key?(resp, "test_pack_for_import")
- admin = insert(:user, info: %{is_admin: true})
+ admin = insert(:user, is_admin: true)
assert conn
|> assign(:user, admin)
|> post(emoji_api_path(conn, :import_from_fs))
|> json_response(200) == ["test_pack_for_import"]
resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
assert resp["test_pack_for_import"]["files"] == %{"blank" => "blank.png"}
File.rm!("#{@emoji_dir_path}/test_pack_for_import/pack.json")
refute File.exists?("#{@emoji_dir_path}/test_pack_for_import/pack.json")
emoji_txt_content = "blank, blank.png, Fun\n\nblank2, blank.png"
File.write!("#{@emoji_dir_path}/test_pack_for_import/emoji.txt", emoji_txt_content)
assert conn
|> assign(:user, admin)
|> post(emoji_api_path(conn, :import_from_fs))
|> json_response(200) == ["test_pack_for_import"]
resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
assert resp["test_pack_for_import"]["files"] == %{
"blank" => "blank.png",
"blank2" => "blank.png"
}
end
end
diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
index 8a6528cbb..0c83edb56 100644
--- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
@@ -1,152 +1,179 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
test "/api/v1/pleroma/conversations/:id", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"})
[participation] = Participation.for_user(other_user)
result =
conn
|> assign(:user, other_user)
|> get("/api/v1/pleroma/conversations/#{participation.id}")
|> json_response(200)
assert result["id"] == participation.id |> to_string()
end
test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "Hi @#{third_user.nickname}!", "visibility" => "direct"})
{:ok, activity} =
CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"})
[participation] = Participation.for_user(other_user)
{:ok, activity_two} =
CommonAPI.post(other_user, %{
"status" => "Hi!",
"in_reply_to_status_id" => activity.id,
"in_reply_to_conversation_id" => participation.id
})
result =
conn
|> assign(:user, other_user)
|> get("/api/v1/pleroma/conversations/#{participation.id}/statuses")
|> json_response(200)
assert length(result) == 2
id_one = activity.id
id_two = activity_two.id
assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result
end
test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} = CommonAPI.post(user, %{"status" => "Hi", "visibility" => "direct"})
[participation] = Participation.for_user(user)
participation = Repo.preload(participation, :recipients)
user = User.get_cached_by_id(user.id)
assert [user] == participation.recipients
assert other_user not in participation.recipients
result =
conn
|> assign(:user, user)
|> patch("/api/v1/pleroma/conversations/#{participation.id}", %{
"recipients" => [user.id, other_user.id]
})
|> json_response(200)
assert result["id"] == participation.id |> to_string
[participation] = Participation.for_user(user)
participation = Repo.preload(participation, :recipients)
assert user in participation.recipients
assert other_user in participation.recipients
end
+ test "POST /api/v1/pleroma/conversations/read", %{conn: conn} do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, _activity} =
+ CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}", "visibility" => "direct"})
+
+ {:ok, _activity} =
+ CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}", "visibility" => "direct"})
+
+ [participation2, participation1] = Participation.for_user(other_user)
+ assert Participation.get(participation2.id).read == false
+ assert Participation.get(participation1.id).read == false
+ assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2
+
+ [%{"unread" => false}, %{"unread" => false}] =
+ conn
+ |> assign(:user, other_user)
+ |> post("/api/v1/pleroma/conversations/read", %{})
+ |> json_response(200)
+
+ [participation2, participation1] = Participation.for_user(other_user)
+ assert Participation.get(participation2.id).read == true
+ assert Participation.get(participation1.id).read == true
+ assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0
+ end
+
describe "POST /api/v1/pleroma/notifications/read" do
test "it marks a single notification as read", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
{:ok, activity1} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"})
{:ok, activity2} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"})
{:ok, [notification1]} = Notification.create_notifications(activity1)
{:ok, [notification2]} = Notification.create_notifications(activity2)
response =
conn
|> assign(:user, user1)
|> post("/api/v1/pleroma/notifications/read", %{"id" => "#{notification1.id}"})
|> json_response(:ok)
assert %{"pleroma" => %{"is_seen" => true}} = response
assert Repo.get(Notification, notification1.id).seen
refute Repo.get(Notification, notification2.id).seen
end
test "it marks multiple notifications as read", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
{:ok, _activity1} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"})
{:ok, _activity2} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"})
{:ok, _activity3} = CommonAPI.post(user2, %{"status" => "HIE @#{user1.nickname}"})
[notification3, notification2, notification1] = Notification.for_user(user1, %{limit: 3})
[response1, response2] =
conn
|> assign(:user, user1)
|> post("/api/v1/pleroma/notifications/read", %{"max_id" => "#{notification2.id}"})
|> json_response(:ok)
assert %{"pleroma" => %{"is_seen" => true}} = response1
assert %{"pleroma" => %{"is_seen" => true}} = response2
assert Repo.get(Notification, notification1.id).seen
assert Repo.get(Notification, notification2.id).seen
refute Repo.get(Notification, notification3.id).seen
end
test "it returns error when notification not found", %{conn: conn} do
user1 = insert(:user)
response =
conn
|> assign(:user, user1)
|> post("/api/v1/pleroma/notifications/read", %{"id" => "22222222222222"})
|> json_response(:bad_request)
assert response == %{"error" => "Cannot get notification"}
end
end
end
diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs
index 2f6ce4bd2..9b554601d 100644
--- a/test/web/push/impl_test.exs
+++ b/test/web/push/impl_test.exs
@@ -1,160 +1,185 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.ImplTest do
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Push.Impl
alias Pleroma.Web.Push.Subscription
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn
%{method: :post, url: "https://example.com/example/1234"} ->
%Tesla.Env{status: 200}
%{method: :post, url: "https://example.com/example/not_found"} ->
%Tesla.Env{status: 400}
%{method: :post, url: "https://example.com/example/bad"} ->
%Tesla.Env{status: 100}
end)
:ok
end
@sub %{
endpoint: "https://example.com/example/1234",
keys: %{
auth: "8eDyX_uCN0XRhSbY5hs7Hg==",
p256dh:
"BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA="
}
}
@api_key "BASgACIHpN1GYgzSRp"
@message "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..."
test "performs sending notifications" do
user = insert(:user)
user2 = insert(:user)
insert(:push_subscription, user: user, data: %{alerts: %{"mention" => true}})
insert(:push_subscription, user: user2, data: %{alerts: %{"mention" => true}})
insert(:push_subscription,
user: user,
data: %{alerts: %{"follow" => true, "mention" => true}}
)
insert(:push_subscription,
user: user,
data: %{alerts: %{"follow" => true, "mention" => false}}
)
{:ok, activity} = CommonAPI.post(user, %{"status" => "<Lorem ipsum dolor sit amet."})
notif =
insert(:notification,
user: user,
activity: activity
)
assert Impl.perform(notif) == [:ok, :ok]
end
@tag capture_log: true
test "returns error if notif does not match " do
assert Impl.perform(%{}) == :error
end
test "successful message sending" do
assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok
end
@tag capture_log: true
test "fail message sending" do
assert Impl.push_message(
@message,
Map.merge(@sub, %{endpoint: "https://example.com/example/bad"}),
@api_key,
%Subscription{}
) == :error
end
- test "delete subsciption if restult send message between 400..500" do
+ test "delete subscription if result send message between 400..500" do
subscription = insert(:push_subscription)
assert Impl.push_message(
@message,
Map.merge(@sub, %{endpoint: "https://example.com/example/not_found"}),
@api_key,
subscription
) == :ok
refute Pleroma.Repo.get(Subscription, subscription.id)
end
- test "renders body for create activity" do
+ test "renders title and body for create activity" do
user = insert(:user, nickname: "Bob")
{:ok, activity} =
CommonAPI.post(user, %{
"status" =>
"<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
})
object = Object.normalize(activity)
assert Impl.format_body(
%{
activity: activity
},
user,
object
) ==
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..."
+
+ assert Impl.format_title(%{activity: activity}) ==
+ "New Mention"
end
- test "renders body for follow activity" do
+ test "renders title and body for follow activity" do
user = insert(:user, nickname: "Bob")
other_user = insert(:user)
{:ok, _, _, activity} = CommonAPI.follow(user, other_user)
object = Object.normalize(activity)
assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has followed you"
+
+ assert Impl.format_title(%{activity: activity}) ==
+ "New Follower"
end
- test "renders body for announce activity" do
+ test "renders title and body for announce activity" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" =>
"<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
})
{:ok, announce_activity, _} = CommonAPI.repeat(activity.id, user)
object = Object.normalize(activity)
assert Impl.format_body(%{activity: announce_activity}, user, object) ==
"@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..."
+
+ assert Impl.format_title(%{activity: announce_activity}) ==
+ "New Repeat"
end
- test "renders body for like activity" do
+ test "renders title and body for like activity" do
user = insert(:user, nickname: "Bob")
{:ok, activity} =
CommonAPI.post(user, %{
"status" =>
"<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
})
{:ok, activity, _} = CommonAPI.favorite(activity.id, user)
object = Object.normalize(activity)
assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post"
+
+ assert Impl.format_title(%{activity: activity}) ==
+ "New Favorite"
+ end
+
+ test "renders title for create activity with direct visibility" do
+ user = insert(:user, nickname: "Bob")
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ "visibility" => "direct",
+ "status" => "This is just between you and me, pal"
+ })
+
+ assert Impl.format_title(%{activity: activity}) ==
+ "New Direct Message"
end
end
diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs
deleted file mode 100644
index 153ec41ac..000000000
--- a/test/web/salmon/salmon_test.exs
+++ /dev/null
@@ -1,101 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Salmon.SalmonTest do
- use Pleroma.DataCase
- alias Pleroma.Activity
- alias Pleroma.Keys
- alias Pleroma.Repo
- alias Pleroma.User
- alias Pleroma.Web.Federator.Publisher
- alias Pleroma.Web.Salmon
- import Mock
- import Pleroma.Factory
-
- @magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB"
-
- @wrong_magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAA"
-
- @magickey_friendica "RSA.AMwa8FUs2fWEjX0xN7yRQgegQffhBpuKNC6fa5VNSVorFjGZhRrlPMn7TQOeihlc9lBz2OsHlIedbYn2uJ7yCs0.AQAB"
-
- setup_all do
- Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- test "decodes a salmon" do
- {:ok, salmon} = File.read("test/fixtures/salmon.xml")
- {:ok, doc} = Salmon.decode_and_validate(@magickey, salmon)
- assert Regex.match?(~r/xml/, doc)
- end
-
- test "errors on wrong magic key" do
- {:ok, salmon} = File.read("test/fixtures/salmon.xml")
- assert Salmon.decode_and_validate(@wrong_magickey, salmon) == :error
- end
-
- test "it encodes a magic key from a public key" do
- key = Salmon.decode_key(@magickey)
- magic_key = Salmon.encode_key(key)
-
- assert @magickey == magic_key
- end
-
- test "it decodes a friendica public key" do
- _key = Salmon.decode_key(@magickey_friendica)
- end
-
- test "encodes an xml payload with a private key" do
- doc = File.read!("test/fixtures/incoming_note_activity.xml")
- pem = File.read!("test/fixtures/private_key.pem")
- {:ok, private, public} = Keys.keys_from_pem(pem)
-
- # Let's try a roundtrip.
- {:ok, salmon} = Salmon.encode(private, doc)
- {:ok, decoded_doc} = Salmon.decode_and_validate(Salmon.encode_key(public), salmon)
-
- assert doc == decoded_doc
- end
-
- test "it gets a magic key" do
- salmon = File.read!("test/fixtures/salmon2.xml")
- {:ok, key} = Salmon.fetch_magic_key(salmon)
-
- assert key ==
- "RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB"
- end
-
- test_with_mock "it pushes an activity to remote accounts it's addressed to",
- Publisher,
- [:passthrough],
- [] do
- user_data = %{
- info: %{
- salmon: "http://test-example.org/salmon"
- },
- local: false
- }
-
- mentioned_user = insert(:user, user_data)
- note = insert(:note)
-
- activity_data = %{
- "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
- "type" => "Create",
- "actor" => note.data["actor"],
- "to" => note.data["to"] ++ [mentioned_user.ap_id],
- "object" => note.data,
- "published_at" => DateTime.utc_now() |> DateTime.to_iso8601(),
- "context" => note.data["context"]
- }
-
- {:ok, activity} = Repo.insert(%Activity{data: activity_data, recipients: activity_data["to"]})
- user = User.get_cached_by_ap_id(activity.data["actor"])
- {:ok, user} = User.ensure_keys_present(user)
-
- Salmon.publish(user, activity)
-
- assert called(Publisher.enqueue_one(Salmon, %{recipient_id: mentioned_user.id}))
- end
-end
diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs
index d33eb1e42..400f3287d 100644
--- a/test/web/streamer/streamer_test.exs
+++ b/test/web/streamer/streamer_test.exs
@@ -1,553 +1,553 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StreamerTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.List
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Streamer
alias Pleroma.Web.Streamer.StreamerSocket
alias Pleroma.Web.Streamer.Worker
@moduletag needs_streamer: true
clear_config_all([:instance, :skip_thread_containment])
describe "user streams" do
setup do
user = insert(:user)
notify = insert(:notification, user: user, activity: build(:note_activity))
{:ok, %{user: user, notify: notify}}
end
test "it sends notify to in the 'user' stream", %{user: user, notify: notify} do
task =
Task.async(fn ->
assert_receive {:text, _}, 4_000
end)
Streamer.add_socket(
"user",
%{transport_pid: task.pid, assigns: %{user: user}}
)
Streamer.stream("user", notify)
Task.await(task)
end
test "it sends notify to in the 'user:notification' stream", %{user: user, notify: notify} do
task =
Task.async(fn ->
assert_receive {:text, _}, 4_000
end)
Streamer.add_socket(
"user:notification",
%{transport_pid: task.pid, assigns: %{user: user}}
)
Streamer.stream("user:notification", notify)
Task.await(task)
end
test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{
user: user
} do
blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
task = Task.async(fn -> refute_receive {:text, _}, 4_000 end)
Streamer.add_socket(
"user:notification",
%{transport_pid: task.pid, assigns: %{user: user}}
)
{:ok, activity} = CommonAPI.post(user, %{"status" => ":("})
{:ok, notif, _} = CommonAPI.favorite(activity.id, blocked)
Streamer.stream("user:notification", notif)
Task.await(task)
end
test "it doesn't send notify to the 'user:notification' stream when a thread is muted", %{
user: user
} do
user2 = insert(:user)
task = Task.async(fn -> refute_receive {:text, _}, 4_000 end)
Streamer.add_socket(
"user:notification",
%{transport_pid: task.pid, assigns: %{user: user}}
)
{:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
{:ok, activity} = CommonAPI.add_mute(user, activity)
{:ok, notif, _} = CommonAPI.favorite(activity.id, user2)
Streamer.stream("user:notification", notif)
Task.await(task)
end
test "it doesn't send notify to the 'user:notification' stream' when a domain is blocked", %{
user: user
} do
user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"})
task = Task.async(fn -> refute_receive {:text, _}, 4_000 end)
Streamer.add_socket(
"user:notification",
%{transport_pid: task.pid, assigns: %{user: user}}
)
{:ok, user} = User.block_domain(user, "hecking-lewd-place.com")
{:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
{:ok, notif, _} = CommonAPI.favorite(activity.id, user2)
Streamer.stream("user:notification", notif)
Task.await(task)
end
end
test "it sends to public" do
user = insert(:user)
other_user = insert(:user)
task =
Task.async(fn ->
assert_receive {:text, _}, 4_000
end)
fake_socket = %StreamerSocket{
transport_pid: task.pid,
user: user
}
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "Test"})
topics = %{
"public" => [fake_socket]
}
Worker.push_to_socket(topics, "public", activity)
Task.await(task)
task =
Task.async(fn ->
expected_event =
%{
"event" => "delete",
"payload" => activity.id
}
|> Jason.encode!()
assert_receive {:text, received_event}, 4_000
assert received_event == expected_event
end)
fake_socket = %StreamerSocket{
transport_pid: task.pid,
user: user
}
{:ok, activity} = CommonAPI.delete(activity.id, other_user)
topics = %{
"public" => [fake_socket]
}
Worker.push_to_socket(topics, "public", activity)
Task.await(task)
end
describe "thread_containment" do
test "it doesn't send to user if recipients invalid and thread containment is enabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], false)
author = insert(:user)
user = insert(:user, following: [author.ap_id])
activity =
insert(:note_activity,
note:
insert(:note,
user: author,
data: %{"to" => ["TEST-FFF"]}
)
)
task = Task.async(fn -> refute_receive {:text, _}, 1_000 end)
fake_socket = %StreamerSocket{transport_pid: task.pid, user: user}
topics = %{"public" => [fake_socket]}
Worker.push_to_socket(topics, "public", activity)
Task.await(task)
end
test "it sends message if recipients invalid and thread containment is disabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], true)
author = insert(:user)
user = insert(:user, following: [author.ap_id])
activity =
insert(:note_activity,
note:
insert(:note,
user: author,
data: %{"to" => ["TEST-FFF"]}
)
)
task = Task.async(fn -> assert_receive {:text, _}, 1_000 end)
fake_socket = %StreamerSocket{transport_pid: task.pid, user: user}
topics = %{"public" => [fake_socket]}
Worker.push_to_socket(topics, "public", activity)
Task.await(task)
end
test "it sends message if recipients invalid and thread containment is enabled but user's thread containment is disabled" do
Pleroma.Config.put([:instance, :skip_thread_containment], false)
author = insert(:user)
- user = insert(:user, following: [author.ap_id], info: %{skip_thread_containment: true})
+ user = insert(:user, following: [author.ap_id], skip_thread_containment: true)
activity =
insert(:note_activity,
note:
insert(:note,
user: author,
data: %{"to" => ["TEST-FFF"]}
)
)
task = Task.async(fn -> assert_receive {:text, _}, 1_000 end)
fake_socket = %StreamerSocket{transport_pid: task.pid, user: user}
topics = %{"public" => [fake_socket]}
Worker.push_to_socket(topics, "public", activity)
Task.await(task)
end
end
describe "blocks" do
test "it doesn't send messages involving blocked users" do
user = insert(:user)
blocked_user = insert(:user)
{:ok, user} = User.block(user, blocked_user)
task =
Task.async(fn ->
refute_receive {:text, _}, 1_000
end)
fake_socket = %StreamerSocket{
transport_pid: task.pid,
user: user
}
{:ok, activity} = CommonAPI.post(blocked_user, %{"status" => "Test"})
topics = %{
"public" => [fake_socket]
}
Worker.push_to_socket(topics, "public", activity)
Task.await(task)
end
test "it doesn't send messages transitively involving blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
task =
Task.async(fn ->
refute_receive {:text, _}, 1_000
end)
fake_socket = %StreamerSocket{
transport_pid: task.pid,
user: blocker
}
topics = %{
"public" => [fake_socket]
}
{:ok, blocker} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})
Worker.push_to_socket(topics, "public", activity_one)
{:ok, activity_two} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
Worker.push_to_socket(topics, "public", activity_two)
{:ok, activity_three} = CommonAPI.post(blockee, %{"status" => "hey! @#{blocker.nickname}"})
Worker.push_to_socket(topics, "public", activity_three)
Task.await(task)
end
end
test "it doesn't send unwanted DMs to list" do
user_a = insert(:user)
user_b = insert(:user)
user_c = insert(:user)
{:ok, user_a} = User.follow(user_a, user_b)
{:ok, list} = List.create("Test", user_a)
{:ok, list} = List.follow(list, user_b)
task =
Task.async(fn ->
refute_receive {:text, _}, 1_000
end)
fake_socket = %StreamerSocket{
transport_pid: task.pid,
user: user_a
}
{:ok, activity} =
CommonAPI.post(user_b, %{
"status" => "@#{user_c.nickname} Test",
"visibility" => "direct"
})
topics = %{
"list:#{list.id}" => [fake_socket]
}
Worker.handle_call({:stream, "list", activity}, self(), topics)
Task.await(task)
end
test "it doesn't send unwanted private posts to list" do
user_a = insert(:user)
user_b = insert(:user)
{:ok, list} = List.create("Test", user_a)
{:ok, list} = List.follow(list, user_b)
task =
Task.async(fn ->
refute_receive {:text, _}, 1_000
end)
fake_socket = %StreamerSocket{
transport_pid: task.pid,
user: user_a
}
{:ok, activity} =
CommonAPI.post(user_b, %{
"status" => "Test",
"visibility" => "private"
})
topics = %{
"list:#{list.id}" => [fake_socket]
}
Worker.handle_call({:stream, "list", activity}, self(), topics)
Task.await(task)
end
test "it sends wanted private posts to list" do
user_a = insert(:user)
user_b = insert(:user)
{:ok, user_a} = User.follow(user_a, user_b)
{:ok, list} = List.create("Test", user_a)
{:ok, list} = List.follow(list, user_b)
task =
Task.async(fn ->
assert_receive {:text, _}, 1_000
end)
fake_socket = %StreamerSocket{
transport_pid: task.pid,
user: user_a
}
{:ok, activity} =
CommonAPI.post(user_b, %{
"status" => "Test",
"visibility" => "private"
})
Streamer.add_socket(
"list:#{list.id}",
fake_socket
)
Worker.handle_call({:stream, "list", activity}, self(), %{})
Task.await(task)
end
test "it doesn't send muted reblogs" do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
CommonAPI.hide_reblogs(user1, user2)
task =
Task.async(fn ->
refute_receive {:text, _}, 1_000
end)
fake_socket = %StreamerSocket{
transport_pid: task.pid,
user: user1
}
{:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"})
{:ok, announce_activity, _} = CommonAPI.repeat(create_activity.id, user2)
topics = %{
"public" => [fake_socket]
}
Worker.push_to_socket(topics, "public", announce_activity)
Task.await(task)
end
test "it doesn't send posts from muted threads" do
user = insert(:user)
user2 = insert(:user)
{:ok, user2, user, _activity} = CommonAPI.follow(user2, user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
{:ok, activity} = CommonAPI.add_mute(user2, activity)
task = Task.async(fn -> refute_receive {:text, _}, 4_000 end)
Process.sleep(4000)
Streamer.add_socket(
"user",
%{transport_pid: task.pid, assigns: %{user: user2}}
)
Streamer.stream("user", activity)
Task.await(task)
end
describe "direct streams" do
setup do
:ok
end
test "it sends conversation update to the 'direct' stream", %{} do
user = insert(:user)
another_user = insert(:user)
task =
Task.async(fn ->
assert_receive {:text, _received_event}, 4_000
end)
Streamer.add_socket(
"direct",
%{transport_pid: task.pid, assigns: %{user: user}}
)
{:ok, _create_activity} =
CommonAPI.post(another_user, %{
"status" => "hey @#{user.nickname}",
"visibility" => "direct"
})
Task.await(task)
end
test "it doesn't send conversation update to the 'direct' streamj when the last message in the conversation is deleted" do
user = insert(:user)
another_user = insert(:user)
{:ok, create_activity} =
CommonAPI.post(another_user, %{
"status" => "hi @#{user.nickname}",
"visibility" => "direct"
})
task =
Task.async(fn ->
assert_receive {:text, received_event}, 4_000
assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event)
refute_receive {:text, _}, 4_000
end)
Process.sleep(1000)
Streamer.add_socket(
"direct",
%{transport_pid: task.pid, assigns: %{user: user}}
)
{:ok, _} = CommonAPI.delete(create_activity.id, another_user)
Task.await(task)
end
test "it sends conversation update to the 'direct' stream when a message is deleted" do
user = insert(:user)
another_user = insert(:user)
{:ok, create_activity} =
CommonAPI.post(another_user, %{
"status" => "hi @#{user.nickname}",
"visibility" => "direct"
})
{:ok, create_activity2} =
CommonAPI.post(another_user, %{
"status" => "hi @#{user.nickname}",
"in_reply_to_status_id" => create_activity.id,
"visibility" => "direct"
})
task =
Task.async(fn ->
assert_receive {:text, received_event}, 4_000
assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event)
assert_receive {:text, received_event}, 4_000
assert %{"event" => "conversation", "payload" => received_payload} =
Jason.decode!(received_event)
assert %{"last_status" => last_status} = Jason.decode!(received_payload)
assert last_status["id"] == to_string(create_activity.id)
end)
Process.sleep(1000)
Streamer.add_socket(
"direct",
%{transport_pid: task.pid, assigns: %{user: user}}
)
{:ok, _} = CommonAPI.delete(create_activity2.id, another_user)
Task.await(task)
end
end
end
diff --git a/test/web/twitter_api/password_controller_test.exs b/test/web/twitter_api/password_controller_test.exs
index dc6d4e3e3..840c84a05 100644
--- a/test/web/twitter_api/password_controller_test.exs
+++ b/test/web/twitter_api/password_controller_test.exs
@@ -1,81 +1,81 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.PasswordResetToken
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
import Pleroma.Factory
describe "GET /api/pleroma/password_reset/token" do
test "it returns error when token invalid", %{conn: conn} do
response =
conn
|> get("/api/pleroma/password_reset/token")
|> html_response(:ok)
assert response =~ "<h2>Invalid Token</h2>"
end
test "it shows password reset form", %{conn: conn} do
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
response =
conn
|> get("/api/pleroma/password_reset/#{token.token}")
|> html_response(:ok)
assert response =~ "<h2>Password Reset for #{user.nickname}</h2>"
end
end
describe "POST /api/pleroma/password_reset" do
test "it returns HTTP 200", %{conn: conn} do
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, _access_token} = Token.create_token(insert(:oauth_app), user, %{})
params = %{
"password" => "test",
password_confirmation: "test",
token: token.token
}
response =
conn
|> assign(:user, user)
|> post("/api/pleroma/password_reset", %{data: params})
|> html_response(:ok)
assert response =~ "<h2>Password changed!</h2>"
user = refresh_record(user)
assert Comeonin.Pbkdf2.checkpw("test", user.password_hash)
assert length(Token.get_user_tokens(user)) == 0
end
test "it sets password_reset_pending to false", %{conn: conn} do
- user = insert(:user, info: %{password_reset_pending: true})
+ user = insert(:user, password_reset_pending: true)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, _access_token} = Token.create_token(insert(:oauth_app), user, %{})
params = %{
"password" => "test",
password_confirmation: "test",
token: token.token
}
conn
|> assign(:user, user)
|> post("/api/pleroma/password_reset", %{data: params})
|> html_response(:ok)
- assert User.get_by_id(user.id).info.password_reset_pending == false
+ assert User.get_by_id(user.id).password_reset_pending == false
end
end
end
diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs
index d1d61d11a..85a9be3e0 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -1,448 +1,448 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
use Pleroma.DataCase
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.TwitterAPI.TwitterAPI
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "it registers a new user and returns the user." do
data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"password" => "bear",
"confirm" => "bear"
}
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = User.get_cached_by_nickname("lain")
assert AccountView.render("show.json", %{user: user}) ==
AccountView.render("show.json", %{user: fetched_user})
end
test "it registers a new user with empty string in bio and returns the user." do
data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "",
"password" => "bear",
"confirm" => "bear"
}
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = User.get_cached_by_nickname("lain")
assert AccountView.render("show.json", %{user: user}) ==
AccountView.render("show.json", %{user: fetched_user})
end
test "it sends confirmation email if :account_activation_required is specified in instance config" do
setting = Pleroma.Config.get([:instance, :account_activation_required])
unless setting do
Pleroma.Config.put([:instance, :account_activation_required], true)
on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
end
data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "",
"password" => "bear",
"confirm" => "bear"
}
{:ok, user} = TwitterAPI.register_user(data)
ObanHelpers.perform_all()
- assert user.info.confirmation_pending
+ assert user.confirmation_pending
email = Pleroma.Emails.UserEmail.account_confirmation_email(user)
notify_email = Pleroma.Config.get([:instance, :notify_email])
instance_name = Pleroma.Config.get([:instance, :name])
Swoosh.TestAssertions.assert_email_sent(
from: {instance_name, notify_email},
to: {user.name, user.email},
html_body: email.html_body
)
end
test "it registers a new user and parses mentions in the bio" do
data1 = %{
"nickname" => "john",
"email" => "john@gmail.com",
"fullname" => "John Doe",
"bio" => "test",
"password" => "bear",
"confirm" => "bear"
}
{:ok, user1} = TwitterAPI.register_user(data1)
data2 = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "@john test",
"password" => "bear",
"confirm" => "bear"
}
{:ok, user2} = TwitterAPI.register_user(data2)
expected_text =
~s(<span class="h-card"><a data-user="#{user1.id}" class="u-url mention" href="#{
user1.ap_id
}" rel="ugc">@<span>john</span></a></span> test)
assert user2.bio == expected_text
end
describe "register with one time token" do
setup do
setting = Pleroma.Config.get([:instance, :registrations_open])
if setting do
Pleroma.Config.put([:instance, :registrations_open], false)
on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
end
:ok
end
test "returns user on success" do
{:ok, invite} = UserInviteToken.create_invite()
data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
"fullname" => "Vinny Vinesauce",
"bio" => "streamer",
"password" => "hiptofbees",
"confirm" => "hiptofbees",
"token" => invite.token
}
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = User.get_cached_by_nickname("vinny")
invite = Repo.get_by(UserInviteToken, token: invite.token)
assert invite.used == true
assert AccountView.render("show.json", %{user: user}) ==
AccountView.render("show.json", %{user: fetched_user})
end
test "returns error on invalid token" do
data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => "DudeLetMeInImAFairy"
}
{:error, msg} = TwitterAPI.register_user(data)
assert msg == "Invalid token"
refute User.get_cached_by_nickname("GrimReaper")
end
test "returns error on expired token" do
{:ok, invite} = UserInviteToken.create_invite()
UserInviteToken.update_invite!(invite, used: true)
data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => invite.token
}
{:error, msg} = TwitterAPI.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("GrimReaper")
end
end
describe "registers with date limited token" do
setup do
setting = Pleroma.Config.get([:instance, :registrations_open])
if setting do
Pleroma.Config.put([:instance, :registrations_open], false)
on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
end
data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
"fullname" => "Vinny Vinesauce",
"bio" => "streamer",
"password" => "hiptofbees",
"confirm" => "hiptofbees"
}
check_fn = fn invite ->
data = Map.put(data, "token", invite.token)
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = User.get_cached_by_nickname("vinny")
assert AccountView.render("show.json", %{user: user}) ==
AccountView.render("show.json", %{user: fetched_user})
end
{:ok, data: data, check_fn: check_fn}
end
test "returns user on success", %{check_fn: check_fn} do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today()})
check_fn.(invite)
invite = Repo.get_by(UserInviteToken, token: invite.token)
refute invite.used
end
test "returns user on token which expired tomorrow", %{check_fn: check_fn} do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), 1)})
check_fn.(invite)
invite = Repo.get_by(UserInviteToken, token: invite.token)
refute invite.used
end
test "returns an error on overdue date", %{data: data} do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1)})
data = Map.put(data, "token", invite.token)
{:error, msg} = TwitterAPI.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("vinny")
invite = Repo.get_by(UserInviteToken, token: invite.token)
refute invite.used
end
end
describe "registers with reusable token" do
setup do
setting = Pleroma.Config.get([:instance, :registrations_open])
if setting do
Pleroma.Config.put([:instance, :registrations_open], false)
on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
end
:ok
end
test "returns user on success, after him registration fails" do
{:ok, invite} = UserInviteToken.create_invite(%{max_use: 100})
UserInviteToken.update_invite!(invite, uses: 99)
data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
"fullname" => "Vinny Vinesauce",
"bio" => "streamer",
"password" => "hiptofbees",
"confirm" => "hiptofbees",
"token" => invite.token
}
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = User.get_cached_by_nickname("vinny")
invite = Repo.get_by(UserInviteToken, token: invite.token)
assert invite.used == true
assert AccountView.render("show.json", %{user: user}) ==
AccountView.render("show.json", %{user: fetched_user})
data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => invite.token
}
{:error, msg} = TwitterAPI.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("GrimReaper")
end
end
describe "registers with reusable date limited token" do
setup do
setting = Pleroma.Config.get([:instance, :registrations_open])
if setting do
Pleroma.Config.put([:instance, :registrations_open], false)
on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end)
end
:ok
end
test "returns user on success" do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})
data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
"fullname" => "Vinny Vinesauce",
"bio" => "streamer",
"password" => "hiptofbees",
"confirm" => "hiptofbees",
"token" => invite.token
}
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = User.get_cached_by_nickname("vinny")
invite = Repo.get_by(UserInviteToken, token: invite.token)
refute invite.used
assert AccountView.render("show.json", %{user: user}) ==
AccountView.render("show.json", %{user: fetched_user})
end
test "error after max uses" do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})
UserInviteToken.update_invite!(invite, uses: 99)
data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
"fullname" => "Vinny Vinesauce",
"bio" => "streamer",
"password" => "hiptofbees",
"confirm" => "hiptofbees",
"token" => invite.token
}
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = User.get_cached_by_nickname("vinny")
invite = Repo.get_by(UserInviteToken, token: invite.token)
assert invite.used == true
assert AccountView.render("show.json", %{user: user}) ==
AccountView.render("show.json", %{user: fetched_user})
data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => invite.token
}
{:error, msg} = TwitterAPI.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("GrimReaper")
end
test "returns error on overdue date" do
{:ok, invite} =
UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})
data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => invite.token
}
{:error, msg} = TwitterAPI.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("GrimReaper")
end
test "returns error on with overdue date and after max" do
{:ok, invite} =
UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})
UserInviteToken.update_invite!(invite, uses: 100)
data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => invite.token
}
{:error, msg} = TwitterAPI.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("GrimReaper")
end
end
test "it returns the error on registration problems" do
data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "close the world.",
"password" => "bear"
}
{:error, error_object} = TwitterAPI.register_user(data)
assert is_binary(error_object[:error])
refute User.get_cached_by_nickname("lain")
end
setup do
Supervisor.terminate_child(Pleroma.Supervisor, Cachex)
Supervisor.restart_child(Pleroma.Supervisor, Cachex)
:ok
end
end
diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs
index 9d4cb70f0..246da4da4 100644
--- a/test/web/twitter_api/util_controller_test.exs
+++ b/test/web/twitter_api/util_controller_test.exs
@@ -1,885 +1,885 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import ExUnit.CaptureLog
import Pleroma.Factory
import Mock
setup do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
clear_config([:instance])
clear_config([:frontend_configurations, :pleroma_fe])
clear_config([:user, :deny_follow_blocked])
describe "POST /api/pleroma/follow_import" do
test "it returns HTTP 200", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
response =
conn
|> assign(:user, user1)
|> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"})
|> json_response(:ok)
assert response == "job started"
end
test "it imports follow lists from file", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
with_mocks([
{File, [],
read!: fn "follow_list.txt" ->
"Account address,Show boosts\n#{user2.ap_id},true"
end}
]) do
response =
conn
|> assign(:user, user1)
|> post("/api/pleroma/follow_import", %{"list" => %Plug.Upload{path: "follow_list.txt"}})
|> json_response(:ok)
assert response == "job started"
assert ObanHelpers.member?(
%{
"op" => "follow_import",
"follower_id" => user1.id,
"followed_identifiers" => [user2.ap_id]
},
all_enqueued(worker: Pleroma.Workers.BackgroundWorker)
)
end
end
test "it imports new-style mastodon follow lists", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
response =
conn
|> assign(:user, user1)
|> post("/api/pleroma/follow_import", %{
"list" => "Account address,Show boosts\n#{user2.ap_id},true"
})
|> json_response(:ok)
assert response == "job started"
end
test "requires 'follow' or 'write:follows' permissions", %{conn: conn} do
token1 = insert(:oauth_token, scopes: ["read", "write"])
token2 = insert(:oauth_token, scopes: ["follow"])
token3 = insert(:oauth_token, scopes: ["something"])
another_user = insert(:user)
for token <- [token1, token2, token3] do
conn =
conn
|> put_req_header("authorization", "Bearer #{token.token}")
|> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"})
if token == token3 do
assert %{"error" => "Insufficient permissions: follow | write:follows."} ==
json_response(conn, 403)
else
assert json_response(conn, 200)
end
end
end
end
describe "POST /api/pleroma/blocks_import" do
test "it returns HTTP 200", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
response =
conn
|> assign(:user, user1)
|> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"})
|> json_response(:ok)
assert response == "job started"
end
test "it imports blocks users from file", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
with_mocks([
{File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end}
]) do
response =
conn
|> assign(:user, user1)
|> post("/api/pleroma/blocks_import", %{"list" => %Plug.Upload{path: "blocks_list.txt"}})
|> json_response(:ok)
assert response == "job started"
assert ObanHelpers.member?(
%{
"op" => "blocks_import",
"blocker_id" => user1.id,
"blocked_identifiers" => [user2.ap_id, user3.ap_id]
},
all_enqueued(worker: Pleroma.Workers.BackgroundWorker)
)
end
end
end
describe "PUT /api/pleroma/notification_settings" do
test "it updates notification settings", %{conn: conn} do
user = insert(:user)
conn
|> assign(:user, user)
|> put("/api/pleroma/notification_settings", %{
"followers" => false,
"bar" => 1
})
|> json_response(:ok)
user = Repo.get(User, user.id)
assert %{
"followers" => false,
"follows" => true,
"non_follows" => true,
"non_followers" => true
- } == user.info.notification_settings
+ } == user.notification_settings
end
end
describe "GET /api/statusnet/config" do
test "it returns config in xml format", %{conn: conn} do
instance = Pleroma.Config.get(:instance)
response =
conn
|> put_req_header("accept", "application/xml")
|> get("/api/statusnet/config")
|> response(:ok)
assert response ==
"<config>\n<site>\n<name>#{Keyword.get(instance, :name)}</name>\n<site>#{
Pleroma.Web.base_url()
}</site>\n<textlimit>#{Keyword.get(instance, :limit)}</textlimit>\n<closed>#{
!Keyword.get(instance, :registrations_open)
}</closed>\n</site>\n</config>\n"
end
test "it returns config in json format", %{conn: conn} do
instance = Pleroma.Config.get(:instance)
Pleroma.Config.put([:instance, :managed_config], true)
Pleroma.Config.put([:instance, :registrations_open], false)
Pleroma.Config.put([:instance, :invites_enabled], true)
Pleroma.Config.put([:instance, :public], false)
Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
response =
conn
|> put_req_header("accept", "application/json")
|> get("/api/statusnet/config")
|> json_response(:ok)
expected_data = %{
"site" => %{
"accountActivationRequired" => "0",
"closed" => "1",
"description" => Keyword.get(instance, :description),
"invitesEnabled" => "1",
"name" => Keyword.get(instance, :name),
"pleromafe" => %{"theme" => "asuka-hospital"},
"private" => "1",
"safeDMMentionsEnabled" => "0",
"server" => Pleroma.Web.base_url(),
"textlimit" => to_string(Keyword.get(instance, :limit)),
"uploadlimit" => %{
"avatarlimit" => to_string(Keyword.get(instance, :avatar_upload_limit)),
"backgroundlimit" => to_string(Keyword.get(instance, :background_upload_limit)),
"bannerlimit" => to_string(Keyword.get(instance, :banner_upload_limit)),
"uploadlimit" => to_string(Keyword.get(instance, :upload_limit))
},
"vapidPublicKey" => Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
}
}
assert response == expected_data
end
test "returns the state of safe_dm_mentions flag", %{conn: conn} do
Pleroma.Config.put([:instance, :safe_dm_mentions], true)
response =
conn
|> get("/api/statusnet/config.json")
|> json_response(:ok)
assert response["site"]["safeDMMentionsEnabled"] == "1"
Pleroma.Config.put([:instance, :safe_dm_mentions], false)
response =
conn
|> get("/api/statusnet/config.json")
|> json_response(:ok)
assert response["site"]["safeDMMentionsEnabled"] == "0"
end
test "it returns the managed config", %{conn: conn} do
Pleroma.Config.put([:instance, :managed_config], false)
Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
response =
conn
|> get("/api/statusnet/config.json")
|> json_response(:ok)
refute response["site"]["pleromafe"]
Pleroma.Config.put([:instance, :managed_config], true)
response =
conn
|> get("/api/statusnet/config.json")
|> json_response(:ok)
assert response["site"]["pleromafe"] == %{"theme" => "asuka-hospital"}
end
end
describe "GET /api/pleroma/frontend_configurations" do
test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do
config = [
frontend_a: %{
x: 1,
y: 2
},
frontend_b: %{
z: 3
}
]
Pleroma.Config.put(:frontend_configurations, config)
response =
conn
|> get("/api/pleroma/frontend_configurations")
|> json_response(:ok)
assert response == Jason.encode!(config |> Enum.into(%{})) |> Jason.decode!()
end
end
describe "/api/pleroma/emoji" do
test "returns json with custom emoji with tags", %{conn: conn} do
emoji =
conn
|> get("/api/pleroma/emoji")
|> json_response(200)
assert Enum.all?(emoji, fn
{_key,
%{
"image_url" => url,
"tags" => tags
}} ->
is_binary(url) and is_list(tags)
end)
end
end
describe "GET /ostatus_subscribe - remote_follow/2" do
test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do
conn =
get(
conn,
"/ostatus_subscribe?acct=https://mastodon.social/users/emelie/statuses/101849165031453009"
)
assert redirected_to(conn) =~ "/notice/"
end
test "show follow account page if the `acct` is a account link", %{conn: conn} do
response =
get(
conn,
"/ostatus_subscribe?acct=https://mastodon.social/users/emelie"
)
assert html_response(response, 200) =~ "Log in to follow"
end
test "show follow page if the `acct` is a account link", %{conn: conn} do
user = insert(:user)
response =
conn
|> assign(:user, user)
|> get("/ostatus_subscribe?acct=https://mastodon.social/users/emelie")
assert html_response(response, 200) =~ "Remote follow"
end
test "show follow page with error when user cannot fecth by `acct` link", %{conn: conn} do
user = insert(:user)
assert capture_log(fn ->
response =
conn
|> assign(:user, user)
|> get("/ostatus_subscribe?acct=https://mastodon.social/users/not_found")
assert html_response(response, 200) =~ "Error fetching user"
end) =~ "Object has been deleted"
end
end
describe "POST /ostatus_subscribe - do_remote_follow/2 with assigned user " do
test "follows user", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
response =
conn
|> assign(:user, user)
|> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Account followed!"
assert user2.follower_address in refresh_record(user).following
end
test "returns error when user is deactivated", %{conn: conn} do
- user = insert(:user, info: %{deactivated: true})
+ user = insert(:user, deactivated: true)
user2 = insert(:user)
response =
conn
|> assign(:user, user)
|> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Error following account"
end
test "returns error when user is blocked", %{conn: conn} do
Pleroma.Config.put([:user, :deny_follow_blocked], true)
user = insert(:user)
user2 = insert(:user)
{:ok, _user} = Pleroma.User.block(user2, user)
response =
conn
|> assign(:user, user)
|> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Error following account"
end
test "returns error when followee not found", %{conn: conn} do
user = insert(:user)
response =
conn
|> assign(:user, user)
|> post("/ostatus_subscribe", %{"user" => %{"id" => "jimm"}})
|> response(200)
assert response =~ "Error following account"
end
test "returns success result when user already in followers", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user, user2)
response =
conn
|> assign(:user, refresh_record(user))
|> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Account followed!"
end
end
describe "POST /ostatus_subscribe - do_remote_follow/2 without assigned user " do
test "follows", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
response =
conn
|> post("/ostatus_subscribe", %{
"authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
})
|> response(200)
assert response =~ "Account followed!"
assert user2.follower_address in refresh_record(user).following
end
test "returns error when followee not found", %{conn: conn} do
user = insert(:user)
response =
conn
|> post("/ostatus_subscribe", %{
"authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"}
})
|> response(200)
assert response =~ "Error following account"
end
test "returns error when login invalid", %{conn: conn} do
user = insert(:user)
response =
conn
|> post("/ostatus_subscribe", %{
"authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id}
})
|> response(200)
assert response =~ "Wrong username or password"
end
test "returns error when password invalid", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
response =
conn
|> post("/ostatus_subscribe", %{
"authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id}
})
|> response(200)
assert response =~ "Wrong username or password"
end
test "returns error when user is blocked", %{conn: conn} do
Pleroma.Config.put([:user, :deny_follow_blocked], true)
user = insert(:user)
user2 = insert(:user)
{:ok, _user} = Pleroma.User.block(user2, user)
response =
conn
|> post("/ostatus_subscribe", %{
"authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
})
|> response(200)
assert response =~ "Error following account"
end
end
describe "GET /api/pleroma/healthcheck" do
clear_config([:instance, :healthcheck])
test "returns 503 when healthcheck disabled", %{conn: conn} do
Pleroma.Config.put([:instance, :healthcheck], false)
response =
conn
|> get("/api/pleroma/healthcheck")
|> json_response(503)
assert response == %{}
end
test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do
Pleroma.Config.put([:instance, :healthcheck], true)
with_mock Pleroma.Healthcheck,
system_info: fn -> %Pleroma.Healthcheck{healthy: true} end do
response =
conn
|> get("/api/pleroma/healthcheck")
|> json_response(200)
assert %{
"active" => _,
"healthy" => true,
"idle" => _,
"memory_used" => _,
"pool_size" => _
} = response
end
end
test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do
Pleroma.Config.put([:instance, :healthcheck], true)
with_mock Pleroma.Healthcheck,
system_info: fn -> %Pleroma.Healthcheck{healthy: false} end do
response =
conn
|> get("/api/pleroma/healthcheck")
|> json_response(503)
assert %{
"active" => _,
"healthy" => false,
"idle" => _,
"memory_used" => _,
"pool_size" => _
} = response
end
end
end
describe "POST /api/pleroma/disable_account" do
test "it returns HTTP 200", %{conn: conn} do
user = insert(:user)
response =
conn
|> assign(:user, user)
|> post("/api/pleroma/disable_account", %{"password" => "test"})
|> json_response(:ok)
assert response == %{"status" => "success"}
ObanHelpers.perform_all()
user = User.get_cached_by_id(user.id)
- assert user.info.deactivated == true
+ assert user.deactivated == true
end
test "it returns returns when password invalid", %{conn: conn} do
user = insert(:user)
response =
conn
|> assign(:user, user)
|> post("/api/pleroma/disable_account", %{"password" => "test1"})
|> json_response(:ok)
assert response == %{"error" => "Invalid password."}
user = User.get_cached_by_id(user.id)
- refute user.info.deactivated
+ refute user.deactivated
end
end
describe "GET /api/statusnet/version" do
test "it returns version in xml format", %{conn: conn} do
response =
conn
|> put_req_header("accept", "application/xml")
|> get("/api/statusnet/version")
|> response(:ok)
assert response == "<version>#{Pleroma.Application.named_version()}</version>"
end
test "it returns version in json format", %{conn: conn} do
response =
conn
|> put_req_header("accept", "application/json")
|> get("/api/statusnet/version")
|> json_response(:ok)
assert response == "#{Pleroma.Application.named_version()}"
end
end
describe "POST /main/ostatus - remote_subscribe/2" do
test "renders subscribe form", %{conn: conn} do
user = insert(:user)
response =
conn
|> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""})
|> response(:ok)
refute response =~ "Could not find user"
assert response =~ "Remotely follow #{user.nickname}"
end
test "renders subscribe form with error when user not found", %{conn: conn} do
response =
conn
|> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""})
|> response(:ok)
assert response =~ "Could not find user"
refute response =~ "Remotely follow"
end
test "it redirect to webfinger url", %{conn: conn} do
user = insert(:user)
user2 = insert(:user, ap_id: "shp@social.heldscal.la")
conn =
conn
|> post("/main/ostatus", %{
"user" => %{"nickname" => user.nickname, "profile" => user2.ap_id}
})
assert redirected_to(conn) ==
"https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}"
end
test "it renders form with error when use not found", %{conn: conn} do
user2 = insert(:user, ap_id: "shp@social.heldscal.la")
response =
conn
|> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}})
|> response(:ok)
assert response =~ "Something went wrong."
end
end
test "it returns new captcha", %{conn: conn} do
with_mock Pleroma.Captcha,
new: fn -> "test_captcha" end do
resp =
conn
|> get("/api/pleroma/captcha")
|> response(200)
assert resp == "\"test_captcha\""
assert called(Pleroma.Captcha.new())
end
end
defp with_credentials(conn, username, password) do
header_content = "Basic " <> Base.encode64("#{username}:#{password}")
put_req_header(conn, "authorization", header_content)
end
defp valid_user(_context) do
user = insert(:user)
[user: user]
end
describe "POST /api/pleroma/change_email" do
setup [:valid_user]
test "without credentials", %{conn: conn} do
conn = post(conn, "/api/pleroma/change_email")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials and invalid password", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_email", %{
"password" => "hi",
"email" => "test@test.com"
})
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
test "with credentials, valid password and invalid email", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_email", %{
"password" => "test",
"email" => "foobar"
})
assert json_response(conn, 200) == %{"error" => "Email has invalid format."}
end
test "with credentials, valid password and no email", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_email", %{
"password" => "test"
})
assert json_response(conn, 200) == %{"error" => "Email can't be blank."}
end
test "with credentials, valid password and blank email", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_email", %{
"password" => "test",
"email" => ""
})
assert json_response(conn, 200) == %{"error" => "Email can't be blank."}
end
test "with credentials, valid password and non unique email", %{
conn: conn,
user: current_user
} do
user = insert(:user)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_email", %{
"password" => "test",
"email" => user.email
})
assert json_response(conn, 200) == %{"error" => "Email has already been taken."}
end
test "with credentials, valid password and valid email", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_email", %{
"password" => "test",
"email" => "cofe@foobar.com"
})
assert json_response(conn, 200) == %{"status" => "success"}
end
end
describe "POST /api/pleroma/change_password" do
setup [:valid_user]
test "without credentials", %{conn: conn} do
conn = post(conn, "/api/pleroma/change_password")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials and invalid password", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "hi",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
test "with credentials, valid password and new password and confirmation not matching", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "notnewpass"
})
assert json_response(conn, 200) == %{
"error" => "New password does not match confirmation."
}
end
test "with credentials, valid password and invalid new password", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "",
"new_password_confirmation" => ""
})
assert json_response(conn, 200) == %{
"error" => "New password can't be blank."
}
end
test "with credentials, valid password and matching new password and confirmation", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response(conn, 200) == %{"status" => "success"}
fetched_user = User.get_cached_by_id(current_user.id)
assert Comeonin.Pbkdf2.checkpw("newpass", fetched_user.password_hash) == true
end
end
describe "POST /api/pleroma/delete_account" do
setup [:valid_user]
test "without credentials", %{conn: conn} do
conn = post(conn, "/api/pleroma/delete_account")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials and invalid password", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/delete_account", %{"password" => "hi"})
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
test "with credentials and valid password", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/delete_account", %{"password" => "test"})
assert json_response(conn, 200) == %{"status" => "success"}
# Wait a second for the started task to end
:timer.sleep(1000)
end
end
end
diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs
index 696c1bd70..5aa8c73cf 100644
--- a/test/web/web_finger/web_finger_test.exs
+++ b/test/web/web_finger/web_finger_test.exs
@@ -1,138 +1,111 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFingerTest do
use Pleroma.DataCase
alias Pleroma.Web.WebFinger
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "host meta" do
test "returns a link to the xml lrdd" do
host_info = WebFinger.host_meta()
assert String.contains?(host_info, Pleroma.Web.base_url())
end
end
describe "incoming webfinger request" do
test "works for fqns" do
user = insert(:user)
{:ok, result} =
WebFinger.webfinger("#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "XML")
assert is_binary(result)
end
test "works for ap_ids" do
user = insert(:user)
{:ok, result} = WebFinger.webfinger(user.ap_id, "XML")
assert is_binary(result)
end
end
describe "fingering" do
test "returns error when fails parse xml or json" do
user = "invalid_content@social.heldscal.la"
assert {:error, %Jason.DecodeError{}} = WebFinger.finger(user)
end
- test "returns the info for an OStatus user" do
- user = "shp@social.heldscal.la"
-
- {:ok, data} = WebFinger.finger(user)
-
- assert data["magic_key"] ==
- "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB"
-
- assert data["topic"] == "https://social.heldscal.la/api/statuses/user_timeline/29191.atom"
- assert data["subject"] == "acct:shp@social.heldscal.la"
- assert data["salmon"] == "https://social.heldscal.la/main/salmon/user/29191"
- end
-
test "returns the ActivityPub actor URI for an ActivityPub user" do
user = "framasoft@framatube.org"
{:ok, _data} = WebFinger.finger(user)
end
test "returns the ActivityPub actor URI for an ActivityPub user with the ld+json mimetype" do
user = "kaniini@gerzilla.de"
{:ok, data} = WebFinger.finger(user)
assert data["ap_id"] == "https://gerzilla.de/channel/kaniini"
end
- test "returns the correctly for json ostatus users" do
- user = "winterdienst@gnusocial.de"
-
- {:ok, data} = WebFinger.finger(user)
-
- assert data["magic_key"] ==
- "RSA.qfYaxztz7ZELrE4v5WpJrPM99SKI3iv9Y3Tw6nfLGk-4CRljNYqV8IYX2FXjeucC_DKhPNnlF6fXyASpcSmA_qupX9WC66eVhFhZ5OuyBOeLvJ1C4x7Hi7Di8MNBxY3VdQuQR0tTaS_YAZCwASKp7H6XEid3EJpGt0EQZoNzRd8=.AQAB"
-
- assert data["topic"] == "https://gnusocial.de/api/statuses/user_timeline/249296.atom"
- assert data["subject"] == "acct:winterdienst@gnusocial.de"
- assert data["salmon"] == "https://gnusocial.de/main/salmon/user/249296"
- assert data["subscribe_address"] == "https://gnusocial.de/main/ostatussub?profile={uri}"
- end
-
test "it work for AP-only user" do
user = "kpherox@mstdn.jp"
{:ok, data} = WebFinger.finger(user)
assert data["magic_key"] == nil
assert data["salmon"] == nil
assert data["topic"] == "https://mstdn.jp/users/kPherox.atom"
assert data["subject"] == "acct:kPherox@mstdn.jp"
assert data["ap_id"] == "https://mstdn.jp/users/kPherox"
assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}"
end
test "it works for friendica" do
user = "lain@squeet.me"
{:ok, _data} = WebFinger.finger(user)
end
test "it gets the xrd endpoint" do
{:ok, template} = WebFinger.find_lrdd_template("social.heldscal.la")
assert template == "https://social.heldscal.la/.well-known/webfinger?resource={uri}"
end
test "it gets the xrd endpoint for hubzilla" do
{:ok, template} = WebFinger.find_lrdd_template("macgirvin.com")
assert template == "https://macgirvin.com/xrd/?uri={uri}"
end
test "it gets the xrd endpoint for statusnet" do
{:ok, template} = WebFinger.find_lrdd_template("status.alpicola.com")
assert template == "http://status.alpicola.com/main/xrd?uri={uri}"
end
test "it works with idna domains as nickname" do
nickname = "lain@" <> to_string(:idna.encode("zetsubou.みんな"))
{:ok, _data} = WebFinger.finger(nickname)
end
test "it works with idna domains as link" do
ap_id = "https://" <> to_string(:idna.encode("zetsubou.みんな")) <> "/users/lain"
{:ok, _data} = WebFinger.finger(ap_id)
end
end
end
diff --git a/test/web/websub/websub_controller_test.exs b/test/web/websub/websub_controller_test.exs
deleted file mode 100644
index f6d002b3b..000000000
--- a/test/web/websub/websub_controller_test.exs
+++ /dev/null
@@ -1,86 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Websub.WebsubControllerTest do
- use Pleroma.Web.ConnCase
- import Pleroma.Factory
- alias Pleroma.Repo
- alias Pleroma.Web.Websub
- alias Pleroma.Web.Websub.WebsubClientSubscription
-
- clear_config_all([:instance, :federating]) do
- Pleroma.Config.put([:instance, :federating], true)
- end
-
- test "websub subscription request", %{conn: conn} do
- user = insert(:user)
-
- path = Pleroma.Web.OStatus.pubsub_path(user)
-
- data = %{
- "hub.callback": "http://example.org/sub",
- "hub.mode": "subscribe",
- "hub.topic": Pleroma.Web.OStatus.feed_path(user),
- "hub.secret": "a random secret",
- "hub.lease_seconds": "100"
- }
-
- conn =
- conn
- |> post(path, data)
-
- assert response(conn, 202) == "Accepted"
- end
-
- test "websub subscription confirmation", %{conn: conn} do
- websub = insert(:websub_client_subscription)
-
- params = %{
- "hub.mode" => "subscribe",
- "hub.topic" => websub.topic,
- "hub.challenge" => "some challenge",
- "hub.lease_seconds" => "100"
- }
-
- conn =
- conn
- |> get("/push/subscriptions/#{websub.id}", params)
-
- websub = Repo.get(WebsubClientSubscription, websub.id)
-
- assert response(conn, 200) == "some challenge"
- assert websub.state == "accepted"
- assert_in_delta NaiveDateTime.diff(websub.valid_until, NaiveDateTime.utc_now()), 100, 5
- end
-
- describe "websub_incoming" do
- test "accepts incoming feed updates", %{conn: conn} do
- websub = insert(:websub_client_subscription)
- doc = "some stuff"
- signature = Websub.sign(websub.secret, doc)
-
- conn =
- conn
- |> put_req_header("x-hub-signature", "sha1=" <> signature)
- |> put_req_header("content-type", "application/atom+xml")
- |> post("/push/subscriptions/#{websub.id}", doc)
-
- assert response(conn, 200) == "OK"
- end
-
- test "rejects incoming feed updates with the wrong signature", %{conn: conn} do
- websub = insert(:websub_client_subscription)
- doc = "some stuff"
- signature = Websub.sign("wrong secret", doc)
-
- conn =
- conn
- |> put_req_header("x-hub-signature", "sha1=" <> signature)
- |> put_req_header("content-type", "application/atom+xml")
- |> post("/push/subscriptions/#{websub.id}", doc)
-
- assert response(conn, 500) == "Error"
- end
- end
-end
diff --git a/test/web/websub/websub_test.exs b/test/web/websub/websub_test.exs
deleted file mode 100644
index 46ca545de..000000000
--- a/test/web/websub/websub_test.exs
+++ /dev/null
@@ -1,236 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.WebsubTest do
- use Pleroma.DataCase
- use Oban.Testing, repo: Pleroma.Repo
-
- alias Pleroma.Tests.ObanHelpers
- alias Pleroma.Web.Router.Helpers
- alias Pleroma.Web.Websub
- alias Pleroma.Web.Websub.WebsubClientSubscription
- alias Pleroma.Web.Websub.WebsubServerSubscription
- alias Pleroma.Workers.SubscriberWorker
-
- import Pleroma.Factory
- import Tesla.Mock
-
- setup do
- mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
- :ok
- end
-
- test "a verification of a request that is accepted" do
- sub = insert(:websub_subscription)
- topic = sub.topic
-
- getter = fn _path, _headers, options ->
- %{
- "hub.challenge": challenge,
- "hub.lease_seconds": seconds,
- "hub.topic": ^topic,
- "hub.mode": "subscribe"
- } = Keyword.get(options, :params)
-
- assert String.to_integer(seconds) > 0
-
- {:ok,
- %Tesla.Env{
- status: 200,
- body: challenge
- }}
- end
-
- {:ok, sub} = Websub.verify(sub, getter)
- assert sub.state == "active"
- end
-
- test "a verification of a request that doesn't return 200" do
- sub = insert(:websub_subscription)
-
- getter = fn _path, _headers, _options ->
- {:ok,
- %Tesla.Env{
- status: 500,
- body: ""
- }}
- end
-
- {:error, sub} = Websub.verify(sub, getter)
- # Keep the current state.
- assert sub.state == "requested"
- end
-
- test "an incoming subscription request" do
- user = insert(:user)
-
- data = %{
- "hub.callback" => "http://example.org/sub",
- "hub.mode" => "subscribe",
- "hub.topic" => Pleroma.Web.OStatus.feed_path(user),
- "hub.secret" => "a random secret",
- "hub.lease_seconds" => "100"
- }
-
- {:ok, subscription} = Websub.incoming_subscription_request(user, data)
- assert subscription.topic == Pleroma.Web.OStatus.feed_path(user)
- assert subscription.state == "requested"
- assert subscription.secret == "a random secret"
- assert subscription.callback == "http://example.org/sub"
- end
-
- test "an incoming subscription request for an existing subscription" do
- user = insert(:user)
-
- sub =
- insert(:websub_subscription, state: "accepted", topic: Pleroma.Web.OStatus.feed_path(user))
-
- data = %{
- "hub.callback" => sub.callback,
- "hub.mode" => "subscribe",
- "hub.topic" => Pleroma.Web.OStatus.feed_path(user),
- "hub.secret" => "a random secret",
- "hub.lease_seconds" => "100"
- }
-
- {:ok, subscription} = Websub.incoming_subscription_request(user, data)
- assert subscription.topic == Pleroma.Web.OStatus.feed_path(user)
- assert subscription.state == sub.state
- assert subscription.secret == "a random secret"
- assert subscription.callback == sub.callback
- assert length(Repo.all(WebsubServerSubscription)) == 1
- assert subscription.id == sub.id
- end
-
- def accepting_verifier(subscription) do
- {:ok, %{subscription | state: "accepted"}}
- end
-
- test "initiate a subscription for a given user and topic" do
- subscriber = insert(:user)
- user = insert(:user, %{info: %Pleroma.User.Info{topic: "some_topic", hub: "some_hub"}})
-
- {:ok, websub} = Websub.subscribe(subscriber, user, &accepting_verifier/1)
- assert websub.subscribers == [subscriber.ap_id]
- assert websub.topic == "some_topic"
- assert websub.hub == "some_hub"
- assert is_binary(websub.secret)
- assert websub.user == user
- assert websub.state == "accepted"
- end
-
- test "discovers the hub and canonical url" do
- topic = "https://mastodon.social/users/lambadalambda.atom"
-
- {:ok, discovered} = Websub.gather_feed_data(topic)
-
- expected = %{
- "hub" => "https://mastodon.social/api/push",
- "uri" => "https://mastodon.social/users/lambadalambda",
- "nickname" => "lambadalambda",
- "name" => "Critical Value",
- "host" => "mastodon.social",
- "bio" => "a cool dude.",
- "avatar" => %{
- "type" => "Image",
- "url" => [
- %{
- "href" =>
- "https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif?1492379244",
- "mediaType" => "image/gif",
- "type" => "Link"
- }
- ]
- }
- }
-
- assert expected == discovered
- end
-
- test "calls the hub, requests topic" do
- hub = "https://social.heldscal.la/main/push/hub"
- topic = "https://social.heldscal.la/api/statuses/user_timeline/23211.atom"
- websub = insert(:websub_client_subscription, %{hub: hub, topic: topic})
-
- poster = fn ^hub, {:form, data}, _headers ->
- assert Keyword.get(data, :"hub.mode") == "subscribe"
-
- assert Keyword.get(data, :"hub.callback") ==
- Helpers.websub_url(
- Pleroma.Web.Endpoint,
- :websub_subscription_confirmation,
- websub.id
- )
-
- {:ok, %{status: 202}}
- end
-
- task = Task.async(fn -> Websub.request_subscription(websub, poster) end)
-
- change = Ecto.Changeset.change(websub, %{state: "accepted"})
- {:ok, _} = Repo.update(change)
-
- {:ok, websub} = Task.await(task)
-
- assert websub.state == "accepted"
- end
-
- test "rejects the subscription if it can't be accepted" do
- hub = "https://social.heldscal.la/main/push/hub"
- topic = "https://social.heldscal.la/api/statuses/user_timeline/23211.atom"
- websub = insert(:websub_client_subscription, %{hub: hub, topic: topic})
-
- poster = fn ^hub, {:form, _data}, _headers ->
- {:ok, %{status: 202}}
- end
-
- {:error, websub} = Websub.request_subscription(websub, poster, 1000)
- assert websub.state == "rejected"
-
- websub = insert(:websub_client_subscription, %{hub: hub, topic: topic})
-
- poster = fn ^hub, {:form, _data}, _headers ->
- {:ok, %{status: 400}}
- end
-
- {:error, websub} = Websub.request_subscription(websub, poster, 1000)
- assert websub.state == "rejected"
- end
-
- test "sign a text" do
- signed = Websub.sign("secret", "text")
- assert signed == "B8392C23690CCF871F37EC270BE1582DEC57A503" |> String.downcase()
-
- _signed = Websub.sign("secret", [["て"], ['す']])
- end
-
- describe "renewing subscriptions" do
- test "it renews subscriptions that have less than a day of time left" do
- day = 60 * 60 * 24
- now = NaiveDateTime.utc_now()
-
- still_good =
- insert(:websub_client_subscription, %{
- valid_until: NaiveDateTime.add(now, 2 * day),
- topic: "http://example.org/still_good",
- hub: "http://example.org/still_good",
- state: "accepted"
- })
-
- needs_refresh =
- insert(:websub_client_subscription, %{
- valid_until: NaiveDateTime.add(now, day - 100),
- topic: "http://example.org/needs_refresh",
- hub: "http://example.org/needs_refresh",
- state: "accepted"
- })
-
- _refresh = Websub.refresh_subscriptions()
- ObanHelpers.perform(all_enqueued(worker: SubscriberWorker))
-
- assert still_good == Repo.get(WebsubClientSubscription, still_good.id)
- refute needs_refresh == Repo.get(WebsubClientSubscription, needs_refresh.id)
- end
- end
-end

File Metadata

Mime Type
text/x-diff
Expires
Wed, May 14, 7:24 AM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
166720
Default Alt Text
(1 MB)

Event Timeline