Merge branch 'develop' into 'remove-avatar-header'
# Conflicts: # CHANGELOG.md
This commit is contained in:
		
						commit
						a0c65bbd6c
					
				
					 237 changed files with 8534 additions and 2226 deletions
				
			
		
							
								
								
									
										1
									
								
								.buildpacks
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.buildpacks
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
https://github.com/hashnuke/heroku-buildpack-elixir  
 | 
			
		||||
							
								
								
									
										152
									
								
								.gitlab-ci.yml
									
									
									
									
									
								
							
							
						
						
									
										152
									
								
								.gitlab-ci.yml
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -16,6 +16,7 @@ stages:
 | 
			
		|||
  - build
 | 
			
		||||
  - test
 | 
			
		||||
  - deploy
 | 
			
		||||
  - release
 | 
			
		||||
 | 
			
		||||
before_script:
 | 
			
		||||
  - mix local.hex --force
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +43,7 @@ docs-build:
 | 
			
		|||
    paths:
 | 
			
		||||
      - priv/static/doc
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
unit-testing:
 | 
			
		||||
  stage: test
 | 
			
		||||
  services:
 | 
			
		||||
| 
						 | 
				
			
			@ -52,8 +54,7 @@ unit-testing:
 | 
			
		|||
    - mix deps.get
 | 
			
		||||
    - mix ecto.create
 | 
			
		||||
    - mix ecto.migrate
 | 
			
		||||
    - mix test --trace --preload-modules
 | 
			
		||||
    - mix coveralls
 | 
			
		||||
    - mix coveralls --trace --preload-modules
 | 
			
		||||
 | 
			
		||||
unit-testing-rum:
 | 
			
		||||
  stage: test
 | 
			
		||||
| 
						 | 
				
			
			@ -95,3 +96,150 @@ docs-deploy:
 | 
			
		|||
    - eval $(ssh-agent -s)
 | 
			
		||||
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
 | 
			
		||||
    - rsync -hrvz --delete -e "ssh -p ${SSH_PORT}" priv/static/doc/ "${SSH_USER_HOST_LOCATION}/${CI_COMMIT_REF_NAME}"
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
  - master@pleroma/pleroma
 | 
			
		||||
  - develop@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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										27
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -4,11 +4,16 @@ 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]
 | 
			
		||||
### Security
 | 
			
		||||
- Mastodon API: Fix display names not being sanitized
 | 
			
		||||
### Added
 | 
			
		||||
- 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
 | 
			
		||||
| 
						 | 
				
			
			@ -16,13 +21,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- 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: `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: `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
 | 
			
		||||
| 
						 | 
				
			
			@ -31,12 +41,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- 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
 | 
			
		||||
| 
						 | 
				
			
			@ -46,9 +60,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- MRF: Support for rejecting reports from specific instances (`mrf_simple`)
 | 
			
		||||
- MRF: Support for stripping avatars and banner images from specific instances (`mrf_simple`)
 | 
			
		||||
- Ability to reset avatar, profile banner and backgroud
 | 
			
		||||
- 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.
 | 
			
		||||
 | 
			
		||||
### Changed
 | 
			
		||||
- **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
 | 
			
		||||
| 
						 | 
				
			
			@ -57,6 +76,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- 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`
 | 
			
		||||
| 
						 | 
				
			
			@ -84,6 +104,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- Respond with a 404 Not implemented JSON error message when requested API is not implemented
 | 
			
		||||
 | 
			
		||||
### 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
 | 
			
		||||
| 
						 | 
				
			
			@ -113,11 +134,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								Procfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								Procfile
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
web: mix phx.server
 | 
			
		||||
release: mix ecto.migrate
 | 
			
		||||
| 
						 | 
				
			
			@ -184,9 +184,6 @@
 | 
			
		|||
  "application/ld+json" => ["activity+json"]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
config :pleroma, :websub, Pleroma.Web.Websub
 | 
			
		||||
config :pleroma, :ostatus, Pleroma.Web.OStatus
 | 
			
		||||
config :pleroma, :httpoison, Pleroma.HTTP
 | 
			
		||||
config :tesla, adapter: Tesla.Adapter.Hackney
 | 
			
		||||
 | 
			
		||||
# Configures http settings, upstream proxy etc.
 | 
			
		||||
| 
						 | 
				
			
			@ -211,6 +208,12 @@
 | 
			
		|||
  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_reachability_timeout_days: 7,
 | 
			
		||||
| 
						 | 
				
			
			@ -240,9 +243,10 @@
 | 
			
		|||
  max_report_comment_size: 1000,
 | 
			
		||||
  safe_dm_mentions: false,
 | 
			
		||||
  healthcheck: false,
 | 
			
		||||
  remote_post_retention_days: 90
 | 
			
		||||
 | 
			
		||||
config :pleroma, :app_account_creation, enabled: true, max_requests: 25, interval: 1800
 | 
			
		||||
  remote_post_retention_days: 90,
 | 
			
		||||
  skip_thread_containment: true,
 | 
			
		||||
  limit_to_local_content: :unauthenticated,
 | 
			
		||||
  dynamic_configuration: false
 | 
			
		||||
 | 
			
		||||
config :pleroma, :markup,
 | 
			
		||||
  # XXX - unfortunately, inline images must be enabled by default right now, because
 | 
			
		||||
| 
						 | 
				
			
			@ -323,6 +327,8 @@
 | 
			
		|||
  federated_timeline_removal: [],
 | 
			
		||||
  replace: []
 | 
			
		||||
 | 
			
		||||
config :pleroma, :mrf_subchain, match_actor: %{}
 | 
			
		||||
 | 
			
		||||
config :pleroma, :rich_media, enabled: true
 | 
			
		||||
 | 
			
		||||
config :pleroma, :media_proxy,
 | 
			
		||||
| 
						 | 
				
			
			@ -355,8 +361,8 @@
 | 
			
		|||
  third_party_engine:
 | 
			
		||||
    "http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-suggestions-api.cgi?{{host}}+{{user}}",
 | 
			
		||||
  timeout: 300_000,
 | 
			
		||||
  limit: 23,
 | 
			
		||||
  web: "https://vinayaka.distsn.org/?{{host}}+{{user}}"
 | 
			
		||||
  limit: 40,
 | 
			
		||||
  web: "https://vinayaka.distsn.org"
 | 
			
		||||
 | 
			
		||||
config :pleroma, :http_security,
 | 
			
		||||
  enabled: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -436,6 +442,8 @@
 | 
			
		|||
  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,
 | 
			
		||||
| 
						 | 
				
			
			@ -456,7 +464,11 @@
 | 
			
		|||
config :esshd,
 | 
			
		||||
  enabled: false
 | 
			
		||||
 | 
			
		||||
oauth_consumer_strategies = String.split(System.get_env("OAUTH_CONSUMER_STRATEGIES") || "")
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -489,9 +501,15 @@
 | 
			
		|||
 | 
			
		||||
config :pleroma, :database, rum_enabled: false
 | 
			
		||||
 | 
			
		||||
config :pleroma, :env, Mix.env()
 | 
			
		||||
 | 
			
		||||
config :http_signatures,
 | 
			
		||||
  adapter: Pleroma.Signature
 | 
			
		||||
 | 
			
		||||
config :pleroma, :rate_limit,
 | 
			
		||||
  search: [{1000, 10}, {1000, 30}],
 | 
			
		||||
  app_account_creation: {1_800_000, 25}
 | 
			
		||||
 | 
			
		||||
# 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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,3 +59,6 @@
 | 
			
		|||
    "!!! RUNNING IN LOCALHOST DEV MODE! !!!\nFEDERATION WON'T WORK UNTIL YOU CONFIGURE A dev.secret.exs"
 | 
			
		||||
  )
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
if File.exists?("./config/dev.exported_from_db.secret.exs"),
 | 
			
		||||
  do: import_config("dev.exported_from_db.secret.exs")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										25
									
								
								config/dokku.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								config/dokku.exs
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
use Mix.Config
 | 
			
		||||
 | 
			
		||||
config :pleroma, Pleroma.Web.Endpoint,
 | 
			
		||||
  http: [
 | 
			
		||||
    port: String.to_integer(System.get_env("PORT") || "4000"),
 | 
			
		||||
    protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]
 | 
			
		||||
  ],
 | 
			
		||||
  protocol: "http",
 | 
			
		||||
  secure_cookie_flag: false,
 | 
			
		||||
  url: [host: System.get_env("APP_HOST"), scheme: "https", port: 443],
 | 
			
		||||
  secret_key_base: "+S+ULgf7+N37c/lc9K66SMphnjQIRGklTu0BRr2vLm2ZzvK0Z6OH/PE77wlUNtvP"
 | 
			
		||||
 | 
			
		||||
database_url =
 | 
			
		||||
  System.get_env("DATABASE_URL") ||
 | 
			
		||||
    raise """
 | 
			
		||||
    environment variable DATABASE_URL is missing.
 | 
			
		||||
    For example: ecto://USER:PASS@HOST/DATABASE
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
config :pleroma, Pleroma.Repo,
 | 
			
		||||
  # ssl: true,
 | 
			
		||||
  url: database_url,
 | 
			
		||||
  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
 | 
			
		||||
 | 
			
		||||
config :pleroma, :instance, name: "#{System.get_env("APP_NAME")} CI Instance"
 | 
			
		||||
| 
						 | 
				
			
			@ -17,8 +17,10 @@
 | 
			
		|||
  http: [port: 4000],
 | 
			
		||||
  protocol: "http"
 | 
			
		||||
 | 
			
		||||
config :phoenix, serve_endpoints: true
 | 
			
		||||
 | 
			
		||||
# Do not print debug messages in production
 | 
			
		||||
config :logger, level: :info
 | 
			
		||||
config :logger, level: :warn
 | 
			
		||||
 | 
			
		||||
# ## SSL Support
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -61,3 +63,6 @@
 | 
			
		|||
# Finally import the config/prod.secret.exs
 | 
			
		||||
# which should be versioned separately.
 | 
			
		||||
import_config "prod.secret.exs"
 | 
			
		||||
 | 
			
		||||
if File.exists?("./config/prod.exported_from_db.secret.exs"),
 | 
			
		||||
  do: import_config("prod.exported_from_db.secret.exs")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										19
									
								
								config/releases.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								config/releases.exs
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
import Config
 | 
			
		||||
 | 
			
		||||
config :pleroma, :instance, static_dir: "/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
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +17,8 @@
 | 
			
		|||
# 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"
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +27,8 @@
 | 
			
		|||
 | 
			
		||||
config :pleroma, :instance,
 | 
			
		||||
  email: "admin@example.com",
 | 
			
		||||
  notify_email: "noreply@example.com"
 | 
			
		||||
  notify_email: "noreply@example.com",
 | 
			
		||||
  skip_thread_containment: false
 | 
			
		||||
 | 
			
		||||
# Configure your database
 | 
			
		||||
config :pleroma, Pleroma.Repo,
 | 
			
		||||
| 
						 | 
				
			
			@ -39,8 +42,6 @@
 | 
			
		|||
# Reduce hash rounds for testing
 | 
			
		||||
config :pbkdf2_elixir, rounds: 1
 | 
			
		||||
 | 
			
		||||
config :pleroma, :websub, Pleroma.Web.WebsubMock
 | 
			
		||||
config :pleroma, :ostatus, Pleroma.Web.OStatusMock
 | 
			
		||||
config :tesla, adapter: Tesla.Mock
 | 
			
		||||
config :pleroma, :rich_media, enabled: false
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +60,7 @@
 | 
			
		|||
  total_user_limit: 3,
 | 
			
		||||
  enabled: false
 | 
			
		||||
 | 
			
		||||
config :pleroma, :app_account_creation, max_requests: 5
 | 
			
		||||
config :pleroma, :rate_limit, app_account_creation: {10_000, 5}
 | 
			
		||||
 | 
			
		||||
config :pleroma, :http_security, report_uri: "https://endpoint.com"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -289,7 +289,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
 | 
			
		|||
  - `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: 
 | 
			
		||||
- 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
 | 
			
		||||
| 
						 | 
				
			
			@ -443,7 +443,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
 | 
			
		|||
- Params:
 | 
			
		||||
  - `id`
 | 
			
		||||
- Response:
 | 
			
		||||
  - On failure: 
 | 
			
		||||
  - On failure:
 | 
			
		||||
    - 403 Forbidden `{"error": "error_msg"}`
 | 
			
		||||
    - 404 Not Found `"Not found"`
 | 
			
		||||
  - On success: JSON, Report object (see above)
 | 
			
		||||
| 
						 | 
				
			
			@ -454,8 +454,8 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
 | 
			
		|||
- Params:
 | 
			
		||||
  - `id`
 | 
			
		||||
  - `state`: required, the new state. Valid values are `open`, `closed` and `resolved`
 | 
			
		||||
- Response: 
 | 
			
		||||
  - On failure: 
 | 
			
		||||
- Response:
 | 
			
		||||
  - On failure:
 | 
			
		||||
    - 400 Bad Request `"Unsupported state"`
 | 
			
		||||
    - 403 Forbidden `{"error": "error_msg"}`
 | 
			
		||||
    - 404 Not Found `"Not found"`
 | 
			
		||||
| 
						 | 
				
			
			@ -467,10 +467,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
 | 
			
		|||
- Params:
 | 
			
		||||
  - `id`
 | 
			
		||||
  - `status`: required, the message
 | 
			
		||||
- Response: 
 | 
			
		||||
  - On failure: 
 | 
			
		||||
    - 400 Bad Request `"Invalid parameters"` when `status` is missing 
 | 
			
		||||
    - 403 Forbidden `{"error": "error_msg"}` 
 | 
			
		||||
- 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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -540,10 +540,10 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
 | 
			
		|||
  - `id`
 | 
			
		||||
  - `sensitive`: optional, valid values are `true` or `false`
 | 
			
		||||
  - `visibility`: optional, valid values are `public`, `private` and `unlisted`
 | 
			
		||||
- Response: 
 | 
			
		||||
  - On failure: 
 | 
			
		||||
- Response:
 | 
			
		||||
  - On failure:
 | 
			
		||||
    - 400 Bad Request `"Unsupported visibility"`
 | 
			
		||||
    - 403 Forbidden `{"error": "error_msg"}` 
 | 
			
		||||
    - 403 Forbidden `{"error": "error_msg"}`
 | 
			
		||||
    - 404 Not Found `"Not found"`
 | 
			
		||||
  - On success: JSON, Mastodon Status entity
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -552,8 +552,88 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
 | 
			
		|||
- Method `DELETE`
 | 
			
		||||
- Params:
 | 
			
		||||
  - `id`
 | 
			
		||||
- Response: 
 | 
			
		||||
  - On failure: 
 | 
			
		||||
    - 403 Forbidden `{"error": "error_msg"}` 
 | 
			
		||||
- Response:
 | 
			
		||||
  - On failure:
 | 
			
		||||
    - 403 Forbidden `{"error": "error_msg"}`
 | 
			
		||||
    - 404 Not Found `"Not found"`
 | 
			
		||||
  - On success: 200 OK `{}`
 | 
			
		||||
 | 
			
		||||
## `/api/pleroma/admin/config`
 | 
			
		||||
### List config settings
 | 
			
		||||
- Method `GET`
 | 
			
		||||
- Params: none
 | 
			
		||||
- Response:
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
  configs: [
 | 
			
		||||
    {
 | 
			
		||||
      "key": string,
 | 
			
		||||
      "value": string or {} or []
 | 
			
		||||
     }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## `/api/pleroma/admin/config`
 | 
			
		||||
### Update config settings
 | 
			
		||||
Module name can be passed as string, which starts with `Pleroma`, e.g. `"Pleroma.Upload"`.
 | 
			
		||||
Atom or boolean value can be passed with `:` in the beginning, e.g. `":true"`, `":upload"`.
 | 
			
		||||
Integer with `i:`, e.g. `"i:150"`.
 | 
			
		||||
 | 
			
		||||
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` => [
 | 
			
		||||
    - `key` (string)
 | 
			
		||||
    - `value` (string, [], {})
 | 
			
		||||
    - `delete` = true (optional, if parameter must be deleted)
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
- Request (example):
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
  configs: [
 | 
			
		||||
    {
 | 
			
		||||
      "key": "Pleroma.Upload",
 | 
			
		||||
      "value": {
 | 
			
		||||
        "uploader": "Pleroma.Uploaders.Local",
 | 
			
		||||
        "filters": ["Pleroma.Upload.Filter.Dedupe"],
 | 
			
		||||
        "link_name": ":true",
 | 
			
		||||
        "proxy_remote": ":false",
 | 
			
		||||
        "proxy_opts": {
 | 
			
		||||
          "redirect_on_failure": ":false",
 | 
			
		||||
          "max_body_length": "i:1048576",
 | 
			
		||||
          "http": {
 | 
			
		||||
            "follow_redirect": ":true",
 | 
			
		||||
            "pool": ":upload"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
     }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
- Response:
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
  configs: [
 | 
			
		||||
    {
 | 
			
		||||
      "key": string,
 | 
			
		||||
      "value": string or {} or []
 | 
			
		||||
     }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,8 @@ Has these additional fields under the `pleroma` object:
 | 
			
		|||
- `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
 | 
			
		||||
- `hide_followers`: boolean, true when the user has follower hiding enabled
 | 
			
		||||
- `hide_follows`: boolean, true when the user has follow hiding enabled
 | 
			
		||||
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
 | 
			
		||||
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
 | 
			
		||||
 | 
			
		||||
### Source
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -69,6 +71,7 @@ Additional parameters can be added to the JSON body/Form data:
 | 
			
		|||
 | 
			
		||||
- `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
 | 
			
		||||
- `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint.
 | 
			
		||||
- `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply.
 | 
			
		||||
 | 
			
		||||
## PATCH `/api/v1/update_credentials`
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -80,6 +83,16 @@ Additional parameters can be added to the JSON body/Form data:
 | 
			
		|||
- `hide_favorites` - if true, user's favorites timeline will be hidden
 | 
			
		||||
- `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API
 | 
			
		||||
- `default_scope` - the scope returned under `privacy` key in Source subentity
 | 
			
		||||
- `pleroma_settings_store` - Opaque user settings to be saved on the backend.
 | 
			
		||||
- `skip_thread_containment` - if true, skip filtering out broken threads
 | 
			
		||||
- `pleroma_background_image` - sets the background image of the user.
 | 
			
		||||
 | 
			
		||||
### Pleroma Settings Store
 | 
			
		||||
Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.
 | 
			
		||||
 | 
			
		||||
The parameter should have a form of `{frontend_name: {...}}`, with `frontend_name` identifying your type of client, e.g. `pleroma_fe`. It will overwrite everything under this property, but will not overwrite other frontend's settings.
 | 
			
		||||
 | 
			
		||||
This information is returned in the `verify_credentials` endpoint.
 | 
			
		||||
 | 
			
		||||
## Authentication
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -126,20 +126,6 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
 | 
			
		|||
## `/api/pleroma/admin/`…
 | 
			
		||||
See [Admin-API](Admin-API.md)
 | 
			
		||||
 | 
			
		||||
## `/api/v1/pleroma/flavour/:flavour`
 | 
			
		||||
* Method `POST`
 | 
			
		||||
* Authentication: required
 | 
			
		||||
* Response: JSON string. Returns the user flavour or the default one on success, otherwise returns `{"error": "error_msg"}`
 | 
			
		||||
* Example response: "glitch"
 | 
			
		||||
* Note: This is intended to be used only by mastofe
 | 
			
		||||
 | 
			
		||||
## `/api/v1/pleroma/flavour`
 | 
			
		||||
* Method `GET`
 | 
			
		||||
* Authentication: required
 | 
			
		||||
* Response: JSON string. Returns the user flavour or the default one.
 | 
			
		||||
* Example response: "glitch"
 | 
			
		||||
* Note: This is intended to be used only by mastofe
 | 
			
		||||
 | 
			
		||||
## `/api/pleroma/notifications/read`
 | 
			
		||||
### Mark a single notification as read
 | 
			
		||||
* Method `POST`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,13 +49,6 @@ Feel free to contact us to be added to this list!
 | 
			
		|||
- Platforms: iOS, Android
 | 
			
		||||
- Features: No Streaming
 | 
			
		||||
 | 
			
		||||
### Tootdon
 | 
			
		||||
- Homepage: <http://tootdon.club/>, <http://blog.mastodon-tootdon.com/>
 | 
			
		||||
- Source Code: ???
 | 
			
		||||
- Contact: [@tootdon@mstdn.jp](https://mstdn.jp/users/tootdon)
 | 
			
		||||
- Platforms: Android, iOS
 | 
			
		||||
- Features: No Streaming
 | 
			
		||||
 | 
			
		||||
### Tusky
 | 
			
		||||
- Homepage: <https://tuskyapp.github.io/>
 | 
			
		||||
- Source Code: <https://github.com/tuskyapp/Tusky>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,6 +71,11 @@ config :pleroma, Pleroma.Emails.Mailer,
 | 
			
		|||
* `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.
 | 
			
		||||
| 
						 | 
				
			
			@ -81,8 +86,11 @@ config :pleroma, Pleroma.Emails.Mailer,
 | 
			
		|||
  * `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.
 | 
			
		||||
* `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``
 | 
			
		||||
| 
						 | 
				
			
			@ -102,15 +110,13 @@ config :pleroma, Pleroma.Emails.Mailer,
 | 
			
		|||
* `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
 | 
			
		||||
* `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.
 | 
			
		||||
* `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`.
 | 
			
		||||
* `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api.
 | 
			
		||||
 | 
			
		||||
## :app_account_creation
 | 
			
		||||
REST API for creating an account settings
 | 
			
		||||
* `enabled`: Enable/disable registration
 | 
			
		||||
* `max_requests`: Number of requests allowed for creating accounts
 | 
			
		||||
* `interval`: Interval for restricting requests for one ip (seconds)
 | 
			
		||||
 | 
			
		||||
## :logger
 | 
			
		||||
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack
 | 
			
		||||
| 
						 | 
				
			
			@ -224,6 +230,21 @@ relates to mascots on the mastodon frontend
 | 
			
		|||
* `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:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -492,7 +513,7 @@ 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.
 | 
			
		||||
* `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>`).
 | 
			
		||||
 | 
			
		||||
## OAuth consumer mode
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -545,6 +566,24 @@ 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
 | 
			
		||||
| 
						 | 
				
			
			@ -575,3 +614,14 @@ To enable them, both the `rum_enabled` flag has to be set and the following spec
 | 
			
		|||
`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`
 | 
			
		||||
 | 
			
		||||
This will probably take a long time.
 | 
			
		||||
 | 
			
		||||
## :rate_limit
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
See [`Pleroma.Plugs.RateLimiter`](Pleroma.Plugs.RateLimiter.html) documentation for examples.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,8 +9,8 @@ config :pleroma, :suggestions,
 | 
			
		|||
  third_party_engine:
 | 
			
		||||
    "http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-suggestions-api.cgi?{{host}}+{{user}}",
 | 
			
		||||
  timeout: 300_000,
 | 
			
		||||
  limit: 23,
 | 
			
		||||
  web: "https://vinayaka.distsn.org/?{{host}}+{{user}}"
 | 
			
		||||
  limit: 40,
 | 
			
		||||
  web: "https://vinayaka.distsn.org"
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +26,6 @@ config :pleroma, :suggestions,
 | 
			
		|||
  third_party_engine:
 | 
			
		||||
    "http://vinayaka.distsn.org/cgi-bin/vinayaka-user-new-suggestions-api.cgi?{{host}}+{{user}}",
 | 
			
		||||
  timeout: 60_000,
 | 
			
		||||
  limit: 23,
 | 
			
		||||
  limit: 40,
 | 
			
		||||
  web: "https://vinayaka.distsn.org/user-new.html"
 | 
			
		||||
```
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -87,7 +87,7 @@ sudo adduser -S -s /bin/false -h /opt/pleroma -H pleroma
 | 
			
		|||
```shell
 | 
			
		||||
sudo mkdir -p /opt/pleroma
 | 
			
		||||
sudo chown -R pleroma:pleroma /opt/pleroma
 | 
			
		||||
sudo -Hu pleroma git clone https://git.pleroma.social/pleroma/pleroma /opt/pleroma
 | 
			
		||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* Change to the new directory:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,7 +66,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
 | 
			
		|||
```shell
 | 
			
		||||
sudo mkdir -p /opt/pleroma
 | 
			
		||||
sudo chown -R pleroma:pleroma /opt/pleroma
 | 
			
		||||
sudo -Hu pleroma git clone https://git.pleroma.social/pleroma/pleroma /opt/pleroma
 | 
			
		||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* Change to the new directory:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -143,7 +143,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
 | 
			
		|||
```shell
 | 
			
		||||
sudo mkdir -p /opt/pleroma
 | 
			
		||||
sudo chown -R pleroma:pleroma /opt/pleroma
 | 
			
		||||
sudo -Hu pleroma git clone https://git.pleroma.social/pleroma/pleroma /opt/pleroma
 | 
			
		||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* Change to the new directory:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
 | 
			
		|||
```shell
 | 
			
		||||
sudo mkdir -p /opt/pleroma
 | 
			
		||||
sudo chown -R pleroma:pleroma /opt/pleroma
 | 
			
		||||
sudo -Hu pleroma git clone https://git.pleroma.social/pleroma/pleroma /opt/pleroma
 | 
			
		||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* Change to the new directory:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,7 +69,7 @@ cd ~
 | 
			
		|||
 | 
			
		||||
*  Gitリポジトリをクローンします。
 | 
			
		||||
```
 | 
			
		||||
git clone https://git.pleroma.social/pleroma/pleroma
 | 
			
		||||
git clone -b master https://git.pleroma.social/pleroma/pleroma
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
*  新しいディレクトリに移動します。
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -106,7 +106,7 @@ It is highly recommended you use your own fork for the `https://path/to/repo` pa
 | 
			
		|||
 | 
			
		||||
```shell
 | 
			
		||||
 pleroma$ cd ~
 | 
			
		||||
 pleroma$ git clone https://path/to/repo
 | 
			
		||||
 pleroma$ git clone -b master https://path/to/repo
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* Change to the new directory:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,7 +58,7 @@ Clone the repository:
 | 
			
		|||
 | 
			
		||||
```
 | 
			
		||||
$ cd /home/pleroma
 | 
			
		||||
$ git clone https://git.pleroma.social/pleroma/pleroma.git
 | 
			
		||||
$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Configure Pleroma. Note that you need a domain name at this point:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ This creates a "pleroma" login class and sets higher values than default for dat
 | 
			
		|||
Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): `useradd -m -L pleroma _pleroma`
 | 
			
		||||
 | 
			
		||||
#### Clone pleroma's directory
 | 
			
		||||
Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide.
 | 
			
		||||
Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b master https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide.
 | 
			
		||||
 | 
			
		||||
#### Postgresql
 | 
			
		||||
Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql:  
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ Vaihda pleroma-käyttäjään ja mene kotihakemistoosi:
 | 
			
		|||
 | 
			
		||||
Lataa pleroman lähdekoodi:
 | 
			
		||||
 | 
			
		||||
`$ git clone https://git.pleroma.social/pleroma/pleroma.git`
 | 
			
		||||
`$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git`
 | 
			
		||||
 | 
			
		||||
`$ cd pleroma`
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								elixir_buildpack.config
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								elixir_buildpack.config
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
elixir_version=1.8.2
 | 
			
		||||
erlang_version=21.3.7
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,9 @@ example.tld  {
 | 
			
		|||
 | 
			
		||||
  gzip
 | 
			
		||||
 | 
			
		||||
  proxy / localhost:4000 {
 | 
			
		||||
  # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
 | 
			
		||||
  # and `localhost.` resolves to [::0] on some systems: see issue #930
 | 
			
		||||
  proxy / 127.0.0.1:4000 {
 | 
			
		||||
    websocket
 | 
			
		||||
    transparent
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,8 +58,10 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
 | 
			
		|||
    RewriteRule /(.*) ws://localhost:4000/$1 [P,L]
 | 
			
		||||
 | 
			
		||||
    ProxyRequests off
 | 
			
		||||
    ProxyPass / http://localhost:4000/
 | 
			
		||||
    ProxyPassReverse / http://localhost:4000/
 | 
			
		||||
    # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
 | 
			
		||||
    # and `localhost.` resolves to [::0] on some systems: see issue #930
 | 
			
		||||
    ProxyPass / http://127.0.0.1:4000/
 | 
			
		||||
    ProxyPassReverse / http://127.0.0.1:4000/
 | 
			
		||||
 | 
			
		||||
    RequestHeader set Host ${servername}
 | 
			
		||||
    ProxyPreserveHost On
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										932
									
								
								installation/pleroma-mongooseim.cfg
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										932
									
								
								installation/pleroma-mongooseim.cfg
									
									
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,932 @@
 | 
			
		|||
%%%
 | 
			
		||||
%%%               ejabberd configuration file
 | 
			
		||||
%%%
 | 
			
		||||
%%%'
 | 
			
		||||
 | 
			
		||||
%%% The parameters used in this configuration file are explained in more detail
 | 
			
		||||
%%% in the ejabberd Installation and Operation Guide.
 | 
			
		||||
%%% Please consult the Guide in case of doubts, it is included with
 | 
			
		||||
%%% your copy of ejabberd, and is also available online at
 | 
			
		||||
%%% http://www.process-one.net/en/ejabberd/docs/
 | 
			
		||||
 | 
			
		||||
%%% This configuration file contains Erlang terms.
 | 
			
		||||
%%% In case you want to understand the syntax, here are the concepts:
 | 
			
		||||
%%%
 | 
			
		||||
%%%  - The character to comment a line is %
 | 
			
		||||
%%%
 | 
			
		||||
%%%  - Each term ends in a dot, for example:
 | 
			
		||||
%%%      override_global.
 | 
			
		||||
%%%
 | 
			
		||||
%%%  - A tuple has a fixed definition, its elements are
 | 
			
		||||
%%%    enclosed in {}, and separated with commas:
 | 
			
		||||
%%%      {loglevel, 4}.
 | 
			
		||||
%%%
 | 
			
		||||
%%%  - A list can have as many elements as you want,
 | 
			
		||||
%%%    and is enclosed in [], for example:
 | 
			
		||||
%%%      [http_poll, web_admin, tls]
 | 
			
		||||
%%%
 | 
			
		||||
%%%    Pay attention that list elements are delimited with commas,
 | 
			
		||||
%%%    but no comma is allowed after the last list element. This will
 | 
			
		||||
%%%    give a syntax error unlike in more lenient languages (e.g. Python).
 | 
			
		||||
%%%
 | 
			
		||||
%%%  - A keyword of ejabberd is a word in lowercase.
 | 
			
		||||
%%%    Strings are enclosed in "" and can contain spaces, dots, ...
 | 
			
		||||
%%%      {language, "en"}.
 | 
			
		||||
%%%      {ldap_rootdn, "dc=example,dc=com"}.
 | 
			
		||||
%%%
 | 
			
		||||
%%%  - This term includes a tuple, a keyword, a list, and two strings:
 | 
			
		||||
%%%      {hosts, ["jabber.example.net", "im.example.com"]}.
 | 
			
		||||
%%%
 | 
			
		||||
%%%  - This config is preprocessed during release generation by a tool which
 | 
			
		||||
%%%    interprets double curly braces as substitution markers, so avoid this
 | 
			
		||||
%%%    syntax in this file (though it's valid Erlang).
 | 
			
		||||
%%%
 | 
			
		||||
%%%    So this is OK (though arguably looks quite ugly):
 | 
			
		||||
%%%      { {s2s_addr, "example-host.net"}, {127,0,0,1} }.
 | 
			
		||||
%%%
 | 
			
		||||
%%%    And I can't give an example of what's not OK exactly because
 | 
			
		||||
%%%    of this rule.
 | 
			
		||||
%%%
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
%%%.   =======================
 | 
			
		||||
%%%'   OVERRIDE STORED OPTIONS
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Override the old values stored in the database.
 | 
			
		||||
%%
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Override global options (shared by all ejabberd nodes in a cluster).
 | 
			
		||||
%%
 | 
			
		||||
%%override_global.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Override local options (specific for this particular ejabberd node).
 | 
			
		||||
%%
 | 
			
		||||
%%override_local.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Remove the Access Control Lists before new ones are added.
 | 
			
		||||
%%
 | 
			
		||||
%%override_acls.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
%%%.   =========
 | 
			
		||||
%%%'   DEBUGGING
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% loglevel: Verbosity of log files generated by ejabberd.
 | 
			
		||||
%% 0: No ejabberd log at all (not recommended)
 | 
			
		||||
%% 1: Critical
 | 
			
		||||
%% 2: Error
 | 
			
		||||
%% 3: Warning
 | 
			
		||||
%% 4: Info
 | 
			
		||||
%% 5: Debug
 | 
			
		||||
%%
 | 
			
		||||
{loglevel, 3}.
 | 
			
		||||
 | 
			
		||||
%%%.   ================
 | 
			
		||||
%%%'   SERVED HOSTNAMES
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% hosts: Domains served by ejabberd.
 | 
			
		||||
%% You can define one or several, for example:
 | 
			
		||||
%% {hosts, ["example.net", "example.com", "example.org"]}.
 | 
			
		||||
%%
 | 
			
		||||
{hosts, ["pleroma.soykaf.com"] }.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% route_subdomains: Delegate subdomains to other XMPP servers.
 | 
			
		||||
%% For example, if this ejabberd serves example.org and you want
 | 
			
		||||
%% to allow communication with an XMPP server called im.example.org.
 | 
			
		||||
%%
 | 
			
		||||
%%{route_subdomains, s2s}.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
%%%.   ===============
 | 
			
		||||
%%%'   LISTENING PORTS
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% listen: The ports ejabberd will listen on, which service each is handled
 | 
			
		||||
%% by and what options to start it with.
 | 
			
		||||
%%
 | 
			
		||||
{listen,
 | 
			
		||||
 [
 | 
			
		||||
  %% BOSH and WS endpoints over HTTP
 | 
			
		||||
  { 5280, ejabberd_cowboy, [
 | 
			
		||||
      {num_acceptors, 10},
 | 
			
		||||
      {transport_options, [{max_connections, 1024}]},
 | 
			
		||||
      {modules, [
 | 
			
		||||
 | 
			
		||||
          {"_", "/http-bind", mod_bosh},
 | 
			
		||||
          {"_", "/ws-xmpp", mod_websockets, [{ejabberd_service, [
 | 
			
		||||
                                        {access, all},
 | 
			
		||||
                                        {shaper_rule, fast},
 | 
			
		||||
                                        {ip, {127, 0, 0, 1}},
 | 
			
		||||
                                        {password, "secret"}]}
 | 
			
		||||
          %% Uncomment to enable connection dropping or/and server-side pings
 | 
			
		||||
          %{timeout, 600000}, {ping_rate, 2000}
 | 
			
		||||
          ]}
 | 
			
		||||
          %% Uncomment to serve static files
 | 
			
		||||
          %{"_", "/static/[...]", cowboy_static,
 | 
			
		||||
          %  {dir, "/var/www", [{mimetypes, cow_mimetypes, all}]}
 | 
			
		||||
          %},
 | 
			
		||||
 | 
			
		||||
          %% Example usage of mod_revproxy
 | 
			
		||||
 | 
			
		||||
          %% {"_", "/[...]", mod_revproxy, [{timeout, 5000},
 | 
			
		||||
          %%                                % time limit for upstream to respond
 | 
			
		||||
          %%                                {body_length, 8000000},
 | 
			
		||||
          %%                                % maximum body size (may be infinity)
 | 
			
		||||
          %%                                {custom_headers, [{<<"header">>,<<"value">>}]}
 | 
			
		||||
          %%                                % list of extra headers that are send to upstream
 | 
			
		||||
          %%                               ]}
 | 
			
		||||
 | 
			
		||||
          %% Example usage of mod_cowboy
 | 
			
		||||
 | 
			
		||||
          %% {"_", "/[...]", mod_cowboy, [{http, mod_revproxy,
 | 
			
		||||
          %%                                [{timeout, 5000},
 | 
			
		||||
          %%                                 % time limit for upstream to respond
 | 
			
		||||
          %%                                 {body_length, 8000000},
 | 
			
		||||
          %%                                 % maximum body size (may be infinity)
 | 
			
		||||
          %%                                 {custom_headers, [{<<"header">>,<<"value">>}]}
 | 
			
		||||
          %%                                 % list of extra headers that are send to upstream
 | 
			
		||||
          %%                                ]},
 | 
			
		||||
          %%                               {ws, xmpp, mod_websockets}
 | 
			
		||||
          %%                             ]}
 | 
			
		||||
      ]}
 | 
			
		||||
  ]},
 | 
			
		||||
 | 
			
		||||
  %% BOSH and WS endpoints over HTTPS
 | 
			
		||||
  { 5285, ejabberd_cowboy, [
 | 
			
		||||
        {num_acceptors, 10},
 | 
			
		||||
        {transport_options, [{max_connections, 1024}]},
 | 
			
		||||
        {ssl, [{certfile, "priv/ssl/fullchain.pem"}, {keyfile, "priv/ssl/privkey.pem"}, {password, ""}]},
 | 
			
		||||
        {modules, [
 | 
			
		||||
            {"_", "/http-bind", mod_bosh},
 | 
			
		||||
            {"_", "/ws-xmpp", mod_websockets, [
 | 
			
		||||
            %% Uncomment to enable connection dropping or/and server-side pings
 | 
			
		||||
            %{timeout, 600000}, {ping_rate, 60000}
 | 
			
		||||
            ]}
 | 
			
		||||
            %% Uncomment to serve static files
 | 
			
		||||
            %{"_", "/static/[...]", cowboy_static,
 | 
			
		||||
            %  {dir, "/var/www", [{mimetypes, cow_mimetypes, all}]}
 | 
			
		||||
            %},
 | 
			
		||||
        ]}
 | 
			
		||||
    ]},
 | 
			
		||||
 | 
			
		||||
  %% MongooseIM HTTP API it's important to start it on localhost
 | 
			
		||||
  %% or some private interface only (not accessible from the outside)
 | 
			
		||||
  %% At least start it on different port which will be hidden behind firewall
 | 
			
		||||
 | 
			
		||||
  { {8088, "127.0.0.1"} , ejabberd_cowboy, [
 | 
			
		||||
      {num_acceptors, 10},
 | 
			
		||||
      {transport_options, [{max_connections, 1024}]},
 | 
			
		||||
      {modules, [
 | 
			
		||||
          {"localhost", "/api", mongoose_api_admin, []}
 | 
			
		||||
      ]}
 | 
			
		||||
  ]},
 | 
			
		||||
 | 
			
		||||
  { 8089 , ejabberd_cowboy, [
 | 
			
		||||
      {num_acceptors, 10},
 | 
			
		||||
      {transport_options, [{max_connections, 1024}]},
 | 
			
		||||
      {protocol_options, [{compress, true}]},
 | 
			
		||||
      {ssl, [{certfile, "priv/ssl/fullchain.pem"}, {keyfile, "priv/ssl/privkey.pem"}, {password, ""}]},
 | 
			
		||||
      {modules, [
 | 
			
		||||
          {"_", "/api/sse", lasse_handler, [mongoose_client_api_sse]},
 | 
			
		||||
          {"_", "/api/messages/[:with]", mongoose_client_api_messages, []},
 | 
			
		||||
          {"_", "/api/contacts/[:jid]", mongoose_client_api_contacts, []},
 | 
			
		||||
          {"_", "/api/rooms/[:id]",    mongoose_client_api_rooms, []},
 | 
			
		||||
          {"_", "/api/rooms/[:id]/config",    mongoose_client_api_rooms_config, []},
 | 
			
		||||
          {"_", "/api/rooms/:id/users/[:user]",    mongoose_client_api_rooms_users, []},
 | 
			
		||||
          {"_", "/api/rooms/[:id]/messages",    mongoose_client_api_rooms_messages, []}
 | 
			
		||||
      ]}
 | 
			
		||||
  ]},
 | 
			
		||||
 | 
			
		||||
  %% Following HTTP API is deprected, the new one abouve should be used instead
 | 
			
		||||
 | 
			
		||||
  { {5288, "127.0.0.1"} , ejabberd_cowboy, [
 | 
			
		||||
      {num_acceptors, 10},
 | 
			
		||||
      {transport_options, [{max_connections, 1024}]},
 | 
			
		||||
      {modules, [
 | 
			
		||||
          {"localhost", "/api", mongoose_api, [{handlers, [mongoose_api_metrics,
 | 
			
		||||
                                                           mongoose_api_users]}]}
 | 
			
		||||
      ]}
 | 
			
		||||
  ]},
 | 
			
		||||
 | 
			
		||||
  { 5222, ejabberd_c2s, [
 | 
			
		||||
 | 
			
		||||
			%%
 | 
			
		||||
			%% If TLS is compiled in and you installed a SSL
 | 
			
		||||
			%% certificate, specify the full path to the
 | 
			
		||||
			%% file and uncomment this line:
 | 
			
		||||
			%%
 | 
			
		||||
                        {certfile, "priv/ssl/both.pem"}, starttls,
 | 
			
		||||
                        
 | 
			
		||||
                        %%{zlib, 10000},
 | 
			
		||||
			%% https://www.openssl.org/docs/apps/ciphers.html#CIPHER_STRINGS
 | 
			
		||||
			%% {ciphers, "DEFAULT:!EXPORT:!LOW:!SSLv2"},
 | 
			
		||||
			{access, c2s},
 | 
			
		||||
			{shaper, c2s_shaper},
 | 
			
		||||
			{max_stanza_size, 65536},
 | 
			
		||||
			{protocol_options, ["no_sslv3"]}
 | 
			
		||||
                        
 | 
			
		||||
		       ]},
 | 
			
		||||
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  %%
 | 
			
		||||
  %% To enable the old SSL connection method on port 5223:
 | 
			
		||||
  %%
 | 
			
		||||
  %%{5223, ejabberd_c2s, [
 | 
			
		||||
  %%			{access, c2s},
 | 
			
		||||
  %%			{shaper, c2s_shaper},
 | 
			
		||||
  %%			{certfile, "/path/to/ssl.pem"}, tls,
 | 
			
		||||
  %%			{max_stanza_size, 65536}
 | 
			
		||||
  %%		       ]},
 | 
			
		||||
 | 
			
		||||
  { 5269, ejabberd_s2s_in, [
 | 
			
		||||
			   {shaper, s2s_shaper},
 | 
			
		||||
			   {max_stanza_size, 131072},
 | 
			
		||||
			   {protocol_options, ["no_sslv3"]}
 | 
			
		||||
			   
 | 
			
		||||
			  ]}
 | 
			
		||||
 | 
			
		||||
  %%
 | 
			
		||||
  %% ejabberd_service: Interact with external components (transports, ...)
 | 
			
		||||
  %%
 | 
			
		||||
  ,{8888, ejabberd_service, [
 | 
			
		||||
                {access, all},
 | 
			
		||||
                {shaper_rule, fast},
 | 
			
		||||
                {ip, {127, 0, 0, 1}},
 | 
			
		||||
                {password, "secret"}
 | 
			
		||||
           ]}
 | 
			
		||||
 | 
			
		||||
  %%
 | 
			
		||||
  %% ejabberd_stun: Handles STUN Binding requests
 | 
			
		||||
  %%
 | 
			
		||||
  %%{ {3478, udp}, ejabberd_stun, []}
 | 
			
		||||
 | 
			
		||||
 ]}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% s2s_use_starttls: Enable STARTTLS + Dialback for S2S connections.
 | 
			
		||||
%% Allowed values are: false optional required required_trusted
 | 
			
		||||
%% You must specify a certificate file.
 | 
			
		||||
%%
 | 
			
		||||
{s2s_use_starttls, optional}.
 | 
			
		||||
%%
 | 
			
		||||
%% s2s_certfile: Specify a certificate file.
 | 
			
		||||
%%
 | 
			
		||||
{s2s_certfile, "priv/ssl/both.pem"}.
 | 
			
		||||
 | 
			
		||||
%% https://www.openssl.org/docs/apps/ciphers.html#CIPHER_STRINGS
 | 
			
		||||
%% {s2s_ciphers, "DEFAULT:!EXPORT:!LOW:!SSLv2"}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% domain_certfile: Specify a different certificate for each served hostname.
 | 
			
		||||
%%
 | 
			
		||||
%%{domain_certfile, "example.org", "/path/to/example_org.pem"}.
 | 
			
		||||
%%{domain_certfile, "example.com", "/path/to/example_com.pem"}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% S2S whitelist or blacklist
 | 
			
		||||
%%
 | 
			
		||||
%% Default s2s policy for undefined hosts.
 | 
			
		||||
%%
 | 
			
		||||
{s2s_default_policy, deny }.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Allow or deny communication with specific servers.
 | 
			
		||||
%%
 | 
			
		||||
%%{ {s2s_host, "goodhost.org"}, allow}.
 | 
			
		||||
%%{ {s2s_host, "badhost.org"}, deny}.
 | 
			
		||||
 | 
			
		||||
{outgoing_s2s_port, 5269 }.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% IP addresses predefined for specific hosts to skip DNS lookups.
 | 
			
		||||
%% Ports defined here take precedence over outgoing_s2s_port.
 | 
			
		||||
%% Examples:
 | 
			
		||||
%%
 | 
			
		||||
%% { {s2s_addr, "example-host.net"}, {127,0,0,1} }.
 | 
			
		||||
%% { {s2s_addr, "example-host.net"}, { {127,0,0,1}, 5269 } }.
 | 
			
		||||
%% { {s2s_addr, "example-host.net"}, { {127,0,0,1}, 5269 } }.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Outgoing S2S options
 | 
			
		||||
%%
 | 
			
		||||
%% Preferred address families (which to try first) and connect timeout
 | 
			
		||||
%% in milliseconds.
 | 
			
		||||
%%
 | 
			
		||||
%%{outgoing_s2s_options, [ipv4, ipv6], 10000}.
 | 
			
		||||
%%
 | 
			
		||||
%%%.   ==============
 | 
			
		||||
%%%'   SESSION BACKEND
 | 
			
		||||
 | 
			
		||||
%%{sm_backend, {mnesia, []}}.
 | 
			
		||||
 | 
			
		||||
%% Requires {redis, global, default, ..., ...} outgoing pool
 | 
			
		||||
%%{sm_backend, {redis, []}}.
 | 
			
		||||
 | 
			
		||||
{sm_backend, {mnesia, []} }.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
%%%.   ==============
 | 
			
		||||
%%%'   AUTHENTICATION
 | 
			
		||||
 | 
			
		||||
%% Advertised SASL mechanisms
 | 
			
		||||
{sasl_mechanisms, [cyrsasl_plain]}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% auth_method: Method used to authenticate the users.
 | 
			
		||||
%% The default method is the internal.
 | 
			
		||||
%% If you want to use a different method,
 | 
			
		||||
%% comment this line and enable the correct ones.
 | 
			
		||||
%%
 | 
			
		||||
%% {auth_method, internal }.
 | 
			
		||||
{auth_method, http }.
 | 
			
		||||
{auth_opts, [
 | 
			
		||||
             {http, global, auth, [{workers, 50}], [{server, "https://pleroma.soykaf.com"}]},
 | 
			
		||||
             {password_format, plain} % default
 | 
			
		||||
             %% {password_format, scram}
 | 
			
		||||
             
 | 
			
		||||
             %% {scram_iterations, 4096} % default
 | 
			
		||||
             
 | 
			
		||||
             %%
 | 
			
		||||
             %% For auth_http:
 | 
			
		||||
             %% {basic_auth, "user:password"}
 | 
			
		||||
             %% {path_prefix, "/"} % default
 | 
			
		||||
             %% auth_http requires {http, Host | global, auth, ..., ...} outgoing pool.
 | 
			
		||||
             %%
 | 
			
		||||
             %% For auth_external
 | 
			
		||||
             %%{extauth_program, "/path/to/authentication/script"}.
 | 
			
		||||
             %%
 | 
			
		||||
             %% For auth_jwt
 | 
			
		||||
             %% {jwt_secret_source, "/path/to/file"},
 | 
			
		||||
             %% {jwt_algorithm, "RS256"},
 | 
			
		||||
             %% {jwt_username_key, user}
 | 
			
		||||
             %% For cyrsasl_external
 | 
			
		||||
             %% {authenticate_with_cn, false}
 | 
			
		||||
             {cyrsasl_external, standard}
 | 
			
		||||
            ]}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Authentication using external script
 | 
			
		||||
%% Make sure the script is executable by ejabberd.
 | 
			
		||||
%%
 | 
			
		||||
%%{auth_method, external}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Authentication using RDBMS
 | 
			
		||||
%% Remember to setup a database in the next section.
 | 
			
		||||
%%
 | 
			
		||||
%%{auth_method, rdbms}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Authentication using LDAP
 | 
			
		||||
%%
 | 
			
		||||
%%{auth_method, ldap}.
 | 
			
		||||
%%
 | 
			
		||||
 | 
			
		||||
%% List of LDAP servers:
 | 
			
		||||
%%{ldap_servers, ["localhost"]}.
 | 
			
		||||
%%
 | 
			
		||||
%% Encryption of connection to LDAP servers:
 | 
			
		||||
%%{ldap_encrypt, none}.
 | 
			
		||||
%%{ldap_encrypt, tls}.
 | 
			
		||||
%%
 | 
			
		||||
%% Port to connect to on LDAP servers:
 | 
			
		||||
%%{ldap_port, 389}.
 | 
			
		||||
%%{ldap_port, 636}.
 | 
			
		||||
%%
 | 
			
		||||
%% LDAP manager:
 | 
			
		||||
%%{ldap_rootdn, "dc=example,dc=com"}.
 | 
			
		||||
%%
 | 
			
		||||
%% Password of LDAP manager:
 | 
			
		||||
%%{ldap_password, "******"}.
 | 
			
		||||
%%
 | 
			
		||||
%% Search base of LDAP directory:
 | 
			
		||||
%%{ldap_base, "dc=example,dc=com"}.
 | 
			
		||||
%%
 | 
			
		||||
%% LDAP attribute that holds user ID:
 | 
			
		||||
%%{ldap_uids, [{"mail", "%u@mail.example.org"}]}.
 | 
			
		||||
%%
 | 
			
		||||
%% LDAP filter:
 | 
			
		||||
%%{ldap_filter, "(objectClass=shadowAccount)"}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Anonymous login support:
 | 
			
		||||
%%   auth_method: anonymous
 | 
			
		||||
%%   anonymous_protocol: sasl_anon | login_anon | both
 | 
			
		||||
%%   allow_multiple_connections: true | false
 | 
			
		||||
%%
 | 
			
		||||
%%{host_config, "public.example.org", [{auth_method, anonymous},
 | 
			
		||||
%%                                     {allow_multiple_connections, false},
 | 
			
		||||
%%                                     {anonymous_protocol, sasl_anon}]}.
 | 
			
		||||
%%
 | 
			
		||||
%% To use both anonymous and internal authentication:
 | 
			
		||||
%%
 | 
			
		||||
%%{host_config, "public.example.org", [{auth_method, [internal, anonymous]}]}.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
%%%.   ==============
 | 
			
		||||
%%%'   OUTGOING CONNECTIONS (e.g. DB)
 | 
			
		||||
 | 
			
		||||
%% Here you may configure all outgoing connections used by MongooseIM,
 | 
			
		||||
%% e.g. to RDBMS (such as MySQL), Riak or external HTTP components.
 | 
			
		||||
%% Default MongooseIM configuration uses only Mnesia (non-Mnesia extensions are disabled),
 | 
			
		||||
%% so no options here are uncommented out of the box.
 | 
			
		||||
%% This section includes configuration examples; for comprehensive guide
 | 
			
		||||
%% please consult MongooseIM documentation, page "Outgoing connections":
 | 
			
		||||
%% - doc/advanced-configuration/outgoing-connections.md
 | 
			
		||||
%% - https://mongooseim.readthedocs.io/en/latest/advanced-configuration/outgoing-connections/
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{outgoing_pools, [
 | 
			
		||||
%  {riak, global, default, [{workers, 5}], [{address, "127.0.0.1"}, {port, 8087}]},
 | 
			
		||||
%  {elastic, global, default, [], [{host, "elastic.host.com"}, {port, 9042}]},
 | 
			
		||||
  {http, global, auth, [{workers, 50}], [{server, "https://pleroma.soykaf.com"}]}
 | 
			
		||||
%  {cassandra, global, default, [{workers, 100}], [{servers, [{"server1", 9042}]}, {keyspace, "big_mongooseim"}]},
 | 
			
		||||
%  {rdbms, global, default, [{workers, 10}], [{server, {mysql, "server", 3306, "database", "username", "password"}}]}
 | 
			
		||||
]}.
 | 
			
		||||
 | 
			
		||||
%% More examples that may be added to outgoing_pools list:
 | 
			
		||||
%%
 | 
			
		||||
%% == MySQL ==
 | 
			
		||||
%%  {rdbms, global, default, [{workers, 10}],
 | 
			
		||||
%%   [{server, {mysql, "server", 3306, "database", "username", "password"}},
 | 
			
		||||
%%    {keepalive_interval, 10}]},
 | 
			
		||||
%% keepalive_interval is optional
 | 
			
		||||
 | 
			
		||||
%% == PostgreSQL ==
 | 
			
		||||
%%  {rdbms, global, default, [{workers, 10}],
 | 
			
		||||
%%   [{server, {pgsql, "server", 5432, "database", "username", "password"}}]},
 | 
			
		||||
 | 
			
		||||
%% == ODBC (MSSQL) ==
 | 
			
		||||
%%  {rdbms, global, default, [{workers, 10}],
 | 
			
		||||
%%   [{server, "DSN=mongooseim;UID=mongooseim;PWD=mongooseim"}]},
 | 
			
		||||
 | 
			
		||||
%% == Elastic Search ==
 | 
			
		||||
%%  {elastic, global, default, [], [{host, "elastic.host.com"}, {port, 9042}]},
 | 
			
		||||
 | 
			
		||||
%% == Riak ==
 | 
			
		||||
%%  {riak, global, default, [{workers, 20}], [{address, "127.0.0.1"}, {port, 8087}]},
 | 
			
		||||
 | 
			
		||||
%% == HTTP ==
 | 
			
		||||
%%  {http, global, conn1, [{workers, 50}], [{server, "http://server:8080"}]},
 | 
			
		||||
 | 
			
		||||
%% == Cassandra ==
 | 
			
		||||
%%  {cassandra, global, default, [{workers, 100}],
 | 
			
		||||
%%    [
 | 
			
		||||
%%      {servers, [
 | 
			
		||||
%%                 {"cassandra_server1.example.com", 9042},
 | 
			
		||||
%%                 {"cassandra_server2.example.com", 9042},
 | 
			
		||||
%%                 {"cassandra_server3.example.com", 9042},
 | 
			
		||||
%%                 {"cassandra_server4.example.com", 9042}
 | 
			
		||||
%%                ]},
 | 
			
		||||
%%      {keyspace, "big_mongooseim"}
 | 
			
		||||
%%    ]}
 | 
			
		||||
 | 
			
		||||
%% == Extra options ==
 | 
			
		||||
%%
 | 
			
		||||
%% If you use PostgreSQL, have a large database, and need a
 | 
			
		||||
%% faster but inexact replacement for "select count(*) from users"
 | 
			
		||||
%%
 | 
			
		||||
%%{pgsql_users_number_estimate, true}.
 | 
			
		||||
%%
 | 
			
		||||
%% rdbms_server_type specifies what database is used over the RDBMS layer
 | 
			
		||||
%% Can take values mssql, pgsql, mysql
 | 
			
		||||
%% In some cases (for example for MAM with pgsql) it is required to set proper value.
 | 
			
		||||
%%
 | 
			
		||||
%% {rdbms_server_type, pgsql}.
 | 
			
		||||
 | 
			
		||||
%%%.   ===============
 | 
			
		||||
%%%'   TRAFFIC SHAPERS
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% The "normal" shaper limits traffic speed to 1000 B/s
 | 
			
		||||
%%
 | 
			
		||||
{shaper, normal, {maxrate, 1000}}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% The "fast" shaper limits traffic speed to 50000 B/s
 | 
			
		||||
%%
 | 
			
		||||
{shaper, fast, {maxrate, 50000}}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% This option specifies the maximum number of elements in the queue
 | 
			
		||||
%% of the FSM. Refer to the documentation for details.
 | 
			
		||||
%%
 | 
			
		||||
{max_fsm_queue, 1000}.
 | 
			
		||||
 | 
			
		||||
%%%.   ====================
 | 
			
		||||
%%%'   ACCESS CONTROL LISTS
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% The 'admin' ACL grants administrative privileges to XMPP accounts.
 | 
			
		||||
%% You can put here as many accounts as you want.
 | 
			
		||||
%%
 | 
			
		||||
%{acl, admin, {user, "alice", "localhost"}}.
 | 
			
		||||
%{acl, admin, {user, "a", "localhost"}}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Blocked users
 | 
			
		||||
%%
 | 
			
		||||
%%{acl, blocked, {user, "baduser", "example.org"}}.
 | 
			
		||||
%%{acl, blocked, {user, "test"}}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Local users: don't modify this line.
 | 
			
		||||
%%
 | 
			
		||||
{acl, local, {user_regexp, ""}}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% More examples of ACLs
 | 
			
		||||
%%
 | 
			
		||||
%%{acl, jabberorg, {server, "jabber.org"}}.
 | 
			
		||||
%%{acl, aleksey, {user, "aleksey", "jabber.ru"}}.
 | 
			
		||||
%%{acl, test, {user_regexp, "^test"}}.
 | 
			
		||||
%%{acl, test, {user_glob, "test*"}}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Define specific ACLs in a virtual host.
 | 
			
		||||
%%
 | 
			
		||||
%%{host_config, "localhost",
 | 
			
		||||
%% [
 | 
			
		||||
%%  {acl, admin, {user, "bob-local", "localhost"}}
 | 
			
		||||
%% ]
 | 
			
		||||
%%}.
 | 
			
		||||
 | 
			
		||||
%%%.   ============
 | 
			
		||||
%%%'   ACCESS RULES
 | 
			
		||||
 | 
			
		||||
%% Maximum number of simultaneous sessions allowed for a single user:
 | 
			
		||||
{access, max_user_sessions, [{10, all}]}.
 | 
			
		||||
 | 
			
		||||
%% Maximum number of offline messages that users can have:
 | 
			
		||||
{access, max_user_offline_messages, [{5000, admin}, {100, all}]}.
 | 
			
		||||
 | 
			
		||||
%% This rule allows access only for local users:
 | 
			
		||||
{access, local, [{allow, local}]}.
 | 
			
		||||
 | 
			
		||||
%% Only non-blocked users can use c2s connections:
 | 
			
		||||
{access, c2s, [{deny, blocked},
 | 
			
		||||
	       {allow, all}]}.
 | 
			
		||||
 | 
			
		||||
%% For C2S connections, all users except admins use the "normal" shaper
 | 
			
		||||
{access, c2s_shaper, [{none, admin},
 | 
			
		||||
		      {normal, all}]}.
 | 
			
		||||
 | 
			
		||||
%% All S2S connections use the "fast" shaper
 | 
			
		||||
{access, s2s_shaper, [{fast, all}]}.
 | 
			
		||||
 | 
			
		||||
%% Admins of this server are also admins of the MUC service:
 | 
			
		||||
{access, muc_admin, [{allow, admin}]}.
 | 
			
		||||
 | 
			
		||||
%% Only accounts of the local ejabberd server can create rooms:
 | 
			
		||||
{access, muc_create, [{allow, local}]}.
 | 
			
		||||
 | 
			
		||||
%% All users are allowed to use the MUC service:
 | 
			
		||||
{access, muc, [{allow, all}]}.
 | 
			
		||||
 | 
			
		||||
%% In-band registration allows registration of any possible username.
 | 
			
		||||
%% To disable in-band registration, replace 'allow' with 'deny'.
 | 
			
		||||
{access, register, [{allow, all}]}.
 | 
			
		||||
 | 
			
		||||
%% By default the frequency of account registrations from the same IP
 | 
			
		||||
%% is limited to 1 account every 10 minutes. To disable, specify: infinity
 | 
			
		||||
{registration_timeout, infinity}.
 | 
			
		||||
 | 
			
		||||
%% Default settings for MAM.
 | 
			
		||||
%% To set non-standard value, replace 'default' with 'allow' or 'deny'.
 | 
			
		||||
%% Only user can access his/her archive by default.
 | 
			
		||||
%% An online user can read room's archive by default.
 | 
			
		||||
%% Only an owner can change settings and purge messages by default.
 | 
			
		||||
%% Empty list (i.e. `[]`) means `[{deny, all}]`.
 | 
			
		||||
{access, mam_set_prefs, [{default, all}]}.
 | 
			
		||||
{access, mam_get_prefs, [{default, all}]}.
 | 
			
		||||
{access, mam_lookup_messages, [{default, all}]}.
 | 
			
		||||
{access, mam_purge_single_message, [{default, all}]}.
 | 
			
		||||
{access, mam_purge_multiple_messages, [{default, all}]}.
 | 
			
		||||
 | 
			
		||||
%% 1 command of the specified type per second.
 | 
			
		||||
{shaper, mam_shaper, {maxrate, 1}}.
 | 
			
		||||
%% This shaper is primeraly for Mnesia overload protection during stress testing.
 | 
			
		||||
%% The limit is 1000 operations of each type per second.
 | 
			
		||||
{shaper, mam_global_shaper, {maxrate, 1000}}.
 | 
			
		||||
 | 
			
		||||
{access, mam_set_prefs_shaper, [{mam_shaper, all}]}.
 | 
			
		||||
{access, mam_get_prefs_shaper, [{mam_shaper, all}]}.
 | 
			
		||||
{access, mam_lookup_messages_shaper, [{mam_shaper, all}]}.
 | 
			
		||||
{access, mam_purge_single_message_shaper, [{mam_shaper, all}]}.
 | 
			
		||||
{access, mam_purge_multiple_messages_shaper, [{mam_shaper, all}]}.
 | 
			
		||||
 | 
			
		||||
{access, mam_set_prefs_global_shaper, [{mam_global_shaper, all}]}.
 | 
			
		||||
{access, mam_get_prefs_global_shaper, [{mam_global_shaper, all}]}.
 | 
			
		||||
{access, mam_lookup_messages_global_shaper, [{mam_global_shaper, all}]}.
 | 
			
		||||
{access, mam_purge_single_message_global_shaper, [{mam_global_shaper, all}]}.
 | 
			
		||||
{access, mam_purge_multiple_messages_global_shaper, [{mam_global_shaper, all}]}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Define specific Access Rules in a virtual host.
 | 
			
		||||
%%
 | 
			
		||||
%%{host_config, "localhost",
 | 
			
		||||
%% [
 | 
			
		||||
%%  {access, c2s, [{allow, admin}, {deny, all}]},
 | 
			
		||||
%%  {access, register, [{deny, all}]}
 | 
			
		||||
%% ]
 | 
			
		||||
%%}.
 | 
			
		||||
 | 
			
		||||
%%%.   ================
 | 
			
		||||
%%%'   DEFAULT LANGUAGE
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% language: Default language used for server messages.
 | 
			
		||||
%%
 | 
			
		||||
{language, "en"}.
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Set a different default language in a virtual host.
 | 
			
		||||
%%
 | 
			
		||||
%%{host_config, "localhost",
 | 
			
		||||
%% [{language, "ru"}]
 | 
			
		||||
%%}.
 | 
			
		||||
 | 
			
		||||
%%%.   ================
 | 
			
		||||
%%%'   MISCELLANEOUS
 | 
			
		||||
 | 
			
		||||
{all_metrics_are_global, false }.
 | 
			
		||||
 | 
			
		||||
%%%.   ========
 | 
			
		||||
%%%'   SERVICES
 | 
			
		||||
 | 
			
		||||
%% Unlike modules, services are started per node and provide either features which are not
 | 
			
		||||
%% related to any particular host, or backend stuff which is used by modules.
 | 
			
		||||
%% This is handled by `mongoose_service` module.
 | 
			
		||||
 | 
			
		||||
{services,
 | 
			
		||||
    [
 | 
			
		||||
        {service_admin_extra, [{submods, [node, accounts, sessions, vcard,
 | 
			
		||||
                                          roster, last, private, stanza, stats]}]}
 | 
			
		||||
    ]
 | 
			
		||||
}.
 | 
			
		||||
 | 
			
		||||
%%%.   =======
 | 
			
		||||
%%%'   MODULES
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Modules enabled in all mongooseim virtual hosts.
 | 
			
		||||
%% For list of possible modules options, check documentation.
 | 
			
		||||
%%
 | 
			
		||||
{modules,
 | 
			
		||||
 [
 | 
			
		||||
 | 
			
		||||
  %% The format for a single route is as follows:
 | 
			
		||||
  %% {Host, Path, Method, Upstream}
 | 
			
		||||
  %%
 | 
			
		||||
  %% "_" can be used as wildcard for Host, Path and Method
 | 
			
		||||
  %% Upstream can be either host (just http(s)://host:port) or uri
 | 
			
		||||
  %% The difference is that host upstreams append whole path while
 | 
			
		||||
  %% uri upstreams append only remainder that follows the matched Path
 | 
			
		||||
  %% (this behaviour is similar to nginx's proxy_pass rules)
 | 
			
		||||
  %%
 | 
			
		||||
  %% Bindings can be used to match certain parts of host or path.
 | 
			
		||||
  %% They will be later overlaid with parts of the upstream uri.
 | 
			
		||||
  %%
 | 
			
		||||
  %% {mod_revproxy,
 | 
			
		||||
  %%    [{routes, [{"www.erlang-solutions.com", "/admin", "_",
 | 
			
		||||
  %%                "https://www.erlang-solutions.com/"},
 | 
			
		||||
  %%               {":var.com", "/:var", "_", "http://localhost:8080/"},
 | 
			
		||||
  %%               {":domain.com", "/", "_", "http://localhost:8080/:domain"}]
 | 
			
		||||
  %%     }]},
 | 
			
		||||
 | 
			
		||||
% {mod_http_upload, [
 | 
			
		||||
    %% Set max file size in bytes. Defaults to 10 MB.
 | 
			
		||||
    %% Disabled if value is `undefined`.
 | 
			
		||||
%   {max_file_size, 1024},
 | 
			
		||||
    %% Use S3 storage backend
 | 
			
		||||
%   {backend, s3},
 | 
			
		||||
    %% Set options for S3 backend
 | 
			
		||||
%   {s3, [
 | 
			
		||||
%     {bucket_url, "http://s3-eu-west-1.amazonaws.com/konbucket2"},
 | 
			
		||||
%     {region, "eu-west-1"},
 | 
			
		||||
%     {access_key_id, "AKIAIAOAONIULXQGMOUA"},
 | 
			
		||||
%     {secret_access_key, "dGhlcmUgYXJlIG5vIGVhc3RlciBlZ2dzIGhlcmVf"}
 | 
			
		||||
%   ]}
 | 
			
		||||
% ]},
 | 
			
		||||
 | 
			
		||||
  {mod_adhoc, []},
 | 
			
		||||
  
 | 
			
		||||
  {mod_disco, [{users_can_see_hidden_services, false}]},
 | 
			
		||||
  {mod_commands, []},
 | 
			
		||||
  {mod_muc_commands, []},
 | 
			
		||||
  {mod_muc_light_commands, []},
 | 
			
		||||
  {mod_last, []},
 | 
			
		||||
  {mod_stream_management, [
 | 
			
		||||
                           % default 100
 | 
			
		||||
                           % size of a buffer of unacked messages
 | 
			
		||||
                           % {buffer_max, 100}
 | 
			
		||||
 | 
			
		||||
                           % default 1 - server sends the ack request after each stanza
 | 
			
		||||
                           % {ack_freq, 1}
 | 
			
		||||
 | 
			
		||||
                           % default: 600 seconds
 | 
			
		||||
                           % {resume_timeout, 600}
 | 
			
		||||
                          ]},
 | 
			
		||||
  %% {mod_muc_light, [{host, "muclight.@HOST@"}]},
 | 
			
		||||
  %% {mod_muc, [{host, "muc.@HOST@"},
 | 
			
		||||
  %%            {access, muc},
 | 
			
		||||
  %%            {access_create, muc_create}
 | 
			
		||||
  %%           ]},
 | 
			
		||||
  %% {mod_muc_log, [
 | 
			
		||||
  %%                {outdir, "/tmp/muclogs"},
 | 
			
		||||
  %%                {access_log, muc}
 | 
			
		||||
  %%               ]},
 | 
			
		||||
  {mod_offline, [{access_max_user_messages, max_user_offline_messages}]},
 | 
			
		||||
  {mod_privacy, []},
 | 
			
		||||
  {mod_blocking, []},
 | 
			
		||||
  {mod_private, []},
 | 
			
		||||
% {mod_private, [{backend, mnesia}]},
 | 
			
		||||
% {mod_private, [{backend, rdbms}]},
 | 
			
		||||
% {mod_register, [
 | 
			
		||||
%		  %%
 | 
			
		||||
%		  %% Set the minimum informational entropy for passwords.
 | 
			
		||||
%		  %%
 | 
			
		||||
%		  %%{password_strength, 32},
 | 
			
		||||
%
 | 
			
		||||
%		  %%
 | 
			
		||||
%		  %% After successful registration, the user receives
 | 
			
		||||
%		  %% a message with this subject and body.
 | 
			
		||||
%		  %%
 | 
			
		||||
%		  {welcome_message, {""}},
 | 
			
		||||
%
 | 
			
		||||
%		  %%
 | 
			
		||||
%		  %% When a user registers, send a notification to
 | 
			
		||||
%		  %% these XMPP accounts.
 | 
			
		||||
%		  %%
 | 
			
		||||
%
 | 
			
		||||
%
 | 
			
		||||
%		  %%
 | 
			
		||||
%		  %% Only clients in the server machine can register accounts
 | 
			
		||||
%		  %%
 | 
			
		||||
%		  {ip_access, [{allow, "127.0.0.0/8"},
 | 
			
		||||
%			       {deny, "0.0.0.0/0"}]},
 | 
			
		||||
%
 | 
			
		||||
%		  %%
 | 
			
		||||
%		  %% Local c2s or remote s2s users cannot register accounts
 | 
			
		||||
%		  %%
 | 
			
		||||
%		  %%{access_from, deny},
 | 
			
		||||
%
 | 
			
		||||
%		  {access, register}
 | 
			
		||||
%		 ]},
 | 
			
		||||
  {mod_roster, []},
 | 
			
		||||
  {mod_sic, []},
 | 
			
		||||
  {mod_vcard, [%{matches, 1},
 | 
			
		||||
%{search, true},
 | 
			
		||||
%{ldap_search_operator, 'or'}, %% either 'or' or 'and'
 | 
			
		||||
%{ldap_binary_search_fields, [<<"PHOTO">>]},
 | 
			
		||||
%% list of binary search fields (as in vcard after mapping)
 | 
			
		||||
{host, "vjud.@HOST@"}
 | 
			
		||||
]},
 | 
			
		||||
  {mod_bosh, []},
 | 
			
		||||
  {mod_carboncopy, []}
 | 
			
		||||
 | 
			
		||||
  %%
 | 
			
		||||
  %% Message Archive Management (MAM, XEP-0313) for registered users and
 | 
			
		||||
  %% Multi-User chats (MUCs).
 | 
			
		||||
  %%
 | 
			
		||||
 | 
			
		||||
% {mod_mam_meta, [
 | 
			
		||||
    %% Use RDBMS backend (default)
 | 
			
		||||
%   {backend, rdbms},
 | 
			
		||||
 | 
			
		||||
    %% Do not store user preferences (default)
 | 
			
		||||
%   {user_prefs_store, false},
 | 
			
		||||
    %% Store user preferences in RDBMS
 | 
			
		||||
%   {user_prefs_store, rdbms},
 | 
			
		||||
    %% Store user preferences in Mnesia (recommended).
 | 
			
		||||
    %% The preferences store will be called each time, as a message is routed.
 | 
			
		||||
    %% That is why Mnesia is better suited for this job.
 | 
			
		||||
%   {user_prefs_store, mnesia},
 | 
			
		||||
 | 
			
		||||
    %% Enables a pool of asynchronous writers. (default)
 | 
			
		||||
    %% Messages will be grouped together based on archive id.
 | 
			
		||||
%   {async_writer, true},
 | 
			
		||||
 | 
			
		||||
    %% Cache information about users (default)
 | 
			
		||||
%   {cache_users, true},
 | 
			
		||||
 | 
			
		||||
    %% Enable archivization for private messages (default)
 | 
			
		||||
%   {pm, [
 | 
			
		||||
      %% Top-level options can be overriden here if needed, for example:
 | 
			
		||||
%     {async_writer, false}
 | 
			
		||||
%   ]},
 | 
			
		||||
 | 
			
		||||
    %%
 | 
			
		||||
    %% Message Archive Management (MAM) for multi-user chats (MUC).
 | 
			
		||||
    %% Enable XEP-0313 for "muc.@HOST@".
 | 
			
		||||
    %%
 | 
			
		||||
%   {muc, [
 | 
			
		||||
%     {host, "muc.@HOST@"}
 | 
			
		||||
      %% As with pm, top-level options can be overriden for MUC archive
 | 
			
		||||
%   ]},
 | 
			
		||||
%
 | 
			
		||||
    %% Do not use a <stanza-id/> element (by default stanzaid is used)
 | 
			
		||||
%   no_stanzaid_element,
 | 
			
		||||
% ]},
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  %%
 | 
			
		||||
  %% MAM configuration examples
 | 
			
		||||
  %%
 | 
			
		||||
 | 
			
		||||
  %% Only MUC, no user-defined preferences, good performance.
 | 
			
		||||
% {mod_mam_meta, [
 | 
			
		||||
%   {backend, rdbms},
 | 
			
		||||
%   {pm, false},
 | 
			
		||||
%   {muc, [
 | 
			
		||||
%     {host, "muc.@HOST@"}
 | 
			
		||||
%   ]}
 | 
			
		||||
% ]},
 | 
			
		||||
 | 
			
		||||
  %% Only archives for c2c messages, good performance.
 | 
			
		||||
% {mod_mam_meta, [
 | 
			
		||||
%   {backend, rdbms},
 | 
			
		||||
%   {pm, [
 | 
			
		||||
%     {user_prefs_store, mnesia}
 | 
			
		||||
%   ]}
 | 
			
		||||
% ]},
 | 
			
		||||
 | 
			
		||||
  %% Basic configuration for c2c messages, bad performance, easy to debug.
 | 
			
		||||
% {mod_mam_meta, [
 | 
			
		||||
%   {backend, rdbms},
 | 
			
		||||
%   {async_writer, false},
 | 
			
		||||
%   {cache_users, false}
 | 
			
		||||
% ]},
 | 
			
		||||
 | 
			
		||||
  %% Cassandra archive for c2c and MUC conversations.
 | 
			
		||||
  %% No custom settings supported (always archive).
 | 
			
		||||
% {mod_mam_meta, [
 | 
			
		||||
%   {backend, cassandra},
 | 
			
		||||
%   {user_prefs_store, cassandra},
 | 
			
		||||
%   {muc, [{host, "muc.@HOST@"}]}
 | 
			
		||||
% ]}
 | 
			
		||||
 | 
			
		||||
% {mod_event_pusher, [
 | 
			
		||||
%   {backends, [
 | 
			
		||||
%     %%
 | 
			
		||||
%     %% Configuration for Amazon SNS notifications.
 | 
			
		||||
%     %%
 | 
			
		||||
%     {sns, [
 | 
			
		||||
%       %% AWS credentials, region and host configuration
 | 
			
		||||
%       {access_key_id, "AKIAJAZYHOIPY6A2PESA"},
 | 
			
		||||
%       {secret_access_key, "c3RvcCBsb29raW5nIGZvciBlYXN0ZXIgZWdncyxr"},
 | 
			
		||||
%       {region, "eu-west-1"},
 | 
			
		||||
%       {account_id, "251423380551"},
 | 
			
		||||
%       {region, "eu-west-1"},
 | 
			
		||||
%       {sns_host, "sns.eu-west-1.amazonaws.com"},
 | 
			
		||||
%
 | 
			
		||||
%       %% Messages from this MUC host will be sent to the SNS topic
 | 
			
		||||
%       {muc_host, "muc.@HOST@"},
 | 
			
		||||
%
 | 
			
		||||
%       %% Plugin module for defining custom message attributes and user identification
 | 
			
		||||
%       {plugin_module, mod_event_pusher_sns_defaults},
 | 
			
		||||
%
 | 
			
		||||
%       %% Topic name configurations. Removing a topic will disable this specific SNS notification
 | 
			
		||||
%       {presence_updates_topic, "user_presence_updated-dev-1"},  %% For presence updates
 | 
			
		||||
%       {pm_messages_topic, "user_message_sent-dev-1"},           %% For private chat messages
 | 
			
		||||
%       {muc_messages_topic, "user_messagegroup_sent-dev-1"}      %% For group chat messages
 | 
			
		||||
%
 | 
			
		||||
%       %% Pool options
 | 
			
		||||
%       {pool_size, 100}, %% Worker pool size for publishing notifications
 | 
			
		||||
%       {publish_retry_count, 2}, %% Retry count in case of publish error
 | 
			
		||||
%       {publish_retry_time_ms, 50} %% Base exponential backoff time (in ms) for publish errors
 | 
			
		||||
%      ]}
 | 
			
		||||
%   ]}
 | 
			
		||||
 | 
			
		||||
]}.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
%%
 | 
			
		||||
%% Enable modules with custom options in a specific virtual host
 | 
			
		||||
%%
 | 
			
		||||
%%{host_config, "localhost",
 | 
			
		||||
%% [{ {add, modules},
 | 
			
		||||
%%   [
 | 
			
		||||
%%    {mod_some_module, []}
 | 
			
		||||
%%   ]
 | 
			
		||||
%%  }
 | 
			
		||||
%% ]}.
 | 
			
		||||
 | 
			
		||||
%%%.
 | 
			
		||||
%%%'
 | 
			
		||||
 | 
			
		||||
%%% $Id$
 | 
			
		||||
 | 
			
		||||
%%% Local Variables:
 | 
			
		||||
%%% mode: erlang
 | 
			
		||||
%%% End:
 | 
			
		||||
%%% vim: set filetype=erlang tabstop=8 foldmarker=%%%',%%%. foldmethod=marker:
 | 
			
		||||
%%%.
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +69,9 @@ server {
 | 
			
		|||
        proxy_set_header Connection "upgrade";
 | 
			
		||||
        proxy_set_header Host $http_host;
 | 
			
		||||
 | 
			
		||||
        proxy_pass http://localhost:4000;
 | 
			
		||||
	# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
 | 
			
		||||
	# and `localhost.` resolves to [::0] on some systems: see issue #930
 | 
			
		||||
        proxy_pass http://127.0.0.1:4000;
 | 
			
		||||
 | 
			
		||||
        client_max_body_size 16m;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
vcl 4.0;
 | 
			
		||||
vcl 4.1;
 | 
			
		||||
import std;
 | 
			
		||||
 | 
			
		||||
backend default {
 | 
			
		||||
| 
						 | 
				
			
			@ -35,24 +35,6 @@ sub vcl_recv {
 | 
			
		|||
      }
 | 
			
		||||
      return(purge);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Pleroma MediaProxy - strip headers that will affect caching
 | 
			
		||||
    if (req.url ~ "^/proxy/") {
 | 
			
		||||
      unset req.http.Cookie;
 | 
			
		||||
      unset req.http.Authorization;
 | 
			
		||||
      unset req.http.Accept;
 | 
			
		||||
      return (hash);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Strip headers that will affect caching from all other static content
 | 
			
		||||
    # This also permits caching of individual toots and AP Activities
 | 
			
		||||
    if ((req.url ~ "^/(media|static)/") ||
 | 
			
		||||
    (req.url ~ "(?i)\.(html|js|css|jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|mp4|ogg|webm|svg|swf|ttf|pdf|woff|woff2)$"))
 | 
			
		||||
    {
 | 
			
		||||
      unset req.http.Cookie;
 | 
			
		||||
      unset req.http.Authorization;
 | 
			
		||||
      return (hash);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sub vcl_backend_response {
 | 
			
		||||
| 
						 | 
				
			
			@ -61,6 +43,12 @@ sub vcl_backend_response {
 | 
			
		|||
      set beresp.do_gzip = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Retry broken backend responses.
 | 
			
		||||
    if (beresp.status == 503) {
 | 
			
		||||
      set bereq.http.X-Varnish-Backend-503 = "1";
 | 
			
		||||
      return (retry);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # CHUNKED SUPPORT
 | 
			
		||||
    if (bereq.http.x-range ~ "bytes=" && beresp.status == 206) {
 | 
			
		||||
      set beresp.ttl = 10m;
 | 
			
		||||
| 
						 | 
				
			
			@ -73,8 +61,6 @@ sub vcl_backend_response {
 | 
			
		|||
      return (deliver);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Default object caching of 86400s;
 | 
			
		||||
    set beresp.ttl = 86400s;
 | 
			
		||||
    # Allow serving cached content for 6h in case backend goes down
 | 
			
		||||
    set beresp.grace = 6h;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -90,20 +76,6 @@ sub vcl_backend_response {
 | 
			
		|||
      set beresp.ttl = 30s;
 | 
			
		||||
      return (deliver);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Pleroma MediaProxy internally sets headers properly
 | 
			
		||||
    if (bereq.url ~ "^/proxy/") {
 | 
			
		||||
      return (deliver);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Strip cache-restricting headers from Pleroma on static content that we want to cache
 | 
			
		||||
    if (bereq.url ~ "(?i)\.(js|css|jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|mp4|ogg|webm|svg|swf|ttf|pdf|woff|woff2)$")
 | 
			
		||||
    {
 | 
			
		||||
      unset beresp.http.set-cookie;
 | 
			
		||||
      unset beresp.http.Cache-Control;
 | 
			
		||||
      unset beresp.http.x-request-id;
 | 
			
		||||
      set beresp.http.Cache-Control = "public, max-age=86400";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# The synthetic response for 301 redirects
 | 
			
		||||
| 
						 | 
				
			
			@ -132,10 +104,32 @@ sub vcl_hash {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
sub vcl_backend_fetch {
 | 
			
		||||
    # Be more lenient for slow servers on the fediverse
 | 
			
		||||
    if bereq.url ~ "^/proxy/" {
 | 
			
		||||
      set bereq.first_byte_timeout = 300s;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # CHUNKED SUPPORT
 | 
			
		||||
    if (bereq.http.x-range) {
 | 
			
		||||
      set bereq.http.Range = bereq.http.x-range;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (bereq.retries == 0) {
 | 
			
		||||
        # Clean up the X-Varnish-Backend-503 flag that is used internally
 | 
			
		||||
        # to mark broken backend responses that should be retried.
 | 
			
		||||
        unset bereq.http.X-Varnish-Backend-503;
 | 
			
		||||
    } else {
 | 
			
		||||
        if (bereq.http.X-Varnish-Backend-503) {
 | 
			
		||||
            if (bereq.method != "POST" &&
 | 
			
		||||
              std.healthy(bereq.backend) &&
 | 
			
		||||
              bereq.retries <= 4) {
 | 
			
		||||
              # Flush broken backend response flag & try again.
 | 
			
		||||
              unset bereq.http.X-Varnish-Backend-503;
 | 
			
		||||
            } else {
 | 
			
		||||
              return (abandon);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sub vcl_deliver {
 | 
			
		||||
| 
						 | 
				
			
			@ -145,3 +139,9 @@ sub vcl_deliver {
 | 
			
		|||
      unset resp.http.CR;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sub vcl_backend_error {
 | 
			
		||||
    # Retry broken backend responses.
 | 
			
		||||
    set bereq.http.X-Varnish-Backend-503 = "1";
 | 
			
		||||
    return (retry);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,13 +29,13 @@ def system_info do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  defp assign_db_info(healthcheck) do
 | 
			
		||||
    database = Application.get_env(:pleroma, Repo)[:database]
 | 
			
		||||
    database = Pleroma.Config.get([Repo, :database])
 | 
			
		||||
 | 
			
		||||
    query =
 | 
			
		||||
      "select state, count(pid) from pg_stat_activity where datname = '#{database}' group by state;"
 | 
			
		||||
 | 
			
		||||
    result = Repo.query!(query)
 | 
			
		||||
    pool_size = Application.get_env(:pleroma, Repo)[:pool_size]
 | 
			
		||||
    pool_size = Pleroma.Config.get([Repo, :pool_size])
 | 
			
		||||
 | 
			
		||||
    db_info =
 | 
			
		||||
      Enum.reduce(result.rows, %{active: 0, idle: 0}, fn [state, cnt], states ->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										67
									
								
								lib/mix/pleroma.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								lib/mix/pleroma.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,67 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Mix.Pleroma do
 | 
			
		||||
  @doc "Common functions to be reused in mix tasks"
 | 
			
		||||
  def start_pleroma do
 | 
			
		||||
    Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
 | 
			
		||||
    {:ok, _} = Application.ensure_all_started(:pleroma)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def load_pleroma do
 | 
			
		||||
    Application.load(:pleroma)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do
 | 
			
		||||
    Keyword.get(options, opt) || shell_prompt(prompt, defval, defname)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def shell_prompt(prompt, defval \\ nil, defname \\ nil) do
 | 
			
		||||
    prompt_message = "#{prompt} [#{defname || defval}] "
 | 
			
		||||
 | 
			
		||||
    input =
 | 
			
		||||
      if mix_shell?(),
 | 
			
		||||
        do: Mix.shell().prompt(prompt_message),
 | 
			
		||||
        else: :io.get_line(prompt_message)
 | 
			
		||||
 | 
			
		||||
    case input do
 | 
			
		||||
      "\n" ->
 | 
			
		||||
        case defval do
 | 
			
		||||
          nil ->
 | 
			
		||||
            shell_prompt(prompt, defval, defname)
 | 
			
		||||
 | 
			
		||||
          defval ->
 | 
			
		||||
            defval
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      input ->
 | 
			
		||||
        String.trim(input)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def shell_yes?(message) do
 | 
			
		||||
    if mix_shell?(),
 | 
			
		||||
      do: Mix.shell().yes?("Continue?"),
 | 
			
		||||
      else: shell_prompt(message, "Continue?") in ~w(Yn Y y)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def shell_info(message) do
 | 
			
		||||
    if mix_shell?(),
 | 
			
		||||
      do: Mix.shell().info(message),
 | 
			
		||||
      else: IO.puts(message)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def shell_error(message) do
 | 
			
		||||
    if mix_shell?(),
 | 
			
		||||
      do: Mix.shell().error(message),
 | 
			
		||||
      else: IO.puts(:stderr, message)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc "Performs a safe check whether `Mix.shell/0` is available (does not raise if Mix is not loaded)"
 | 
			
		||||
  def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0)
 | 
			
		||||
 | 
			
		||||
  def escape_sh_path(path) do
 | 
			
		||||
    ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1,19 +1,19 @@
 | 
			
		|||
defmodule Mix.Tasks.Pleroma.Benchmark do
 | 
			
		||||
  import Mix.Pleroma
 | 
			
		||||
  use Mix.Task
 | 
			
		||||
  alias Mix.Tasks.Pleroma.Common
 | 
			
		||||
 | 
			
		||||
  def run(["search"]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    Benchee.run(%{
 | 
			
		||||
      "search" => fn ->
 | 
			
		||||
        Pleroma.Web.MastodonAPI.MastodonAPIController.status_search(nil, "cofe")
 | 
			
		||||
        Pleroma.Activity.search(nil, "cofe")
 | 
			
		||||
      end
 | 
			
		||||
    })
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["tag"]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    Benchee.run(%{
 | 
			
		||||
      "tag" => fn ->
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +0,0 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Mix.Tasks.Pleroma.Common do
 | 
			
		||||
  @doc "Common functions to be reused in mix tasks"
 | 
			
		||||
  def start_pleroma do
 | 
			
		||||
    Mix.Task.run("app.start")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do
 | 
			
		||||
    Keyword.get(options, opt) ||
 | 
			
		||||
      case Mix.shell().prompt("#{prompt} [#{defname || defval}]") do
 | 
			
		||||
        "\n" ->
 | 
			
		||||
          case defval do
 | 
			
		||||
            nil -> get_option(options, opt, prompt, defval)
 | 
			
		||||
            defval -> defval
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
        opt ->
 | 
			
		||||
          opt |> String.trim()
 | 
			
		||||
      end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def escape_sh_path(path) do
 | 
			
		||||
    ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										69
									
								
								lib/mix/tasks/pleroma/config.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								lib/mix/tasks/pleroma/config.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,69 @@
 | 
			
		|||
defmodule Mix.Tasks.Pleroma.Config do
 | 
			
		||||
  use Mix.Task
 | 
			
		||||
  import Mix.Pleroma
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
  alias Pleroma.Web.AdminAPI.Config
 | 
			
		||||
  @shortdoc "Manages the location of the config"
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Manages the location of the config.
 | 
			
		||||
 | 
			
		||||
  ## Transfers config from file to DB.
 | 
			
		||||
 | 
			
		||||
      mix pleroma.config migrate_to_db
 | 
			
		||||
 | 
			
		||||
  ## Transfers config from DB to file.
 | 
			
		||||
 | 
			
		||||
      mix pleroma.config migrate_from_db ENV
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  def run(["migrate_to_db"]) do
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    if Pleroma.Config.get([:instance, :dynamic_configuration]) do
 | 
			
		||||
      Application.get_all_env(:pleroma)
 | 
			
		||||
      |> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end)
 | 
			
		||||
      |> Enum.each(fn {k, v} ->
 | 
			
		||||
        key = to_string(k) |> String.replace("Elixir.", "")
 | 
			
		||||
        {:ok, _} = Config.update_or_create(%{key: key, value: v})
 | 
			
		||||
        Mix.shell().info("#{key} is migrated.")
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
      Mix.shell().info("Settings migrated.")
 | 
			
		||||
    else
 | 
			
		||||
      Mix.shell().info(
 | 
			
		||||
        "Migration is not allowed by config. You can change this behavior in instance settings."
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["migrate_from_db", env]) do
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    if Pleroma.Config.get([:instance, :dynamic_configuration]) do
 | 
			
		||||
      config_path = "config/#{env}.exported_from_db.secret.exs"
 | 
			
		||||
 | 
			
		||||
      {:ok, file} = File.open(config_path, [:write])
 | 
			
		||||
      IO.write(file, "use Mix.Config\r\n")
 | 
			
		||||
 | 
			
		||||
      Repo.all(Config)
 | 
			
		||||
      |> Enum.each(fn config ->
 | 
			
		||||
        mark = if String.starts_with?(config.key, "Pleroma."), do: ",", else: ":"
 | 
			
		||||
 | 
			
		||||
        IO.write(
 | 
			
		||||
          file,
 | 
			
		||||
          "config :pleroma, #{config.key}#{mark} #{inspect(Config.from_binary(config.value))}\r\n"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        {:ok, _} = Repo.delete(config)
 | 
			
		||||
        Mix.shell().info("#{config.key} deleted from DB.")
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
      File.close(file)
 | 
			
		||||
      System.cmd("mix", ["format", config_path])
 | 
			
		||||
    else
 | 
			
		||||
      Mix.shell().info(
 | 
			
		||||
        "Migration is not allowed by config. You can change this behavior in instance settings."
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -3,12 +3,12 @@
 | 
			
		|||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Mix.Tasks.Pleroma.Database do
 | 
			
		||||
  alias Mix.Tasks.Pleroma.Common
 | 
			
		||||
  alias Pleroma.Conversation
 | 
			
		||||
  alias Pleroma.Object
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  require Logger
 | 
			
		||||
  import Mix.Pleroma
 | 
			
		||||
  use Mix.Task
 | 
			
		||||
 | 
			
		||||
  @shortdoc "A collection of database related tasks"
 | 
			
		||||
| 
						 | 
				
			
			@ -45,7 +45,7 @@ def run(["remove_embedded_objects" | args]) do
 | 
			
		|||
        ]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
    Logger.info("Removing embedded objects")
 | 
			
		||||
 | 
			
		||||
    Repo.query!(
 | 
			
		||||
| 
						 | 
				
			
			@ -66,12 +66,12 @@ def run(["remove_embedded_objects" | args]) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["bump_all_conversations"]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
    Conversation.bump_for_all_activities()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["update_users_following_followers_counts"]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    users = Repo.all(User)
 | 
			
		||||
    Enum.each(users, &User.remove_duplicated_following/1)
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +89,7 @@ def run(["prune_objects" | args]) do
 | 
			
		|||
        ]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    deadline = Pleroma.Config.get([:instance, :remote_post_retention_days])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										49
									
								
								lib/mix/tasks/pleroma/ecto/ecto.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								lib/mix/tasks/pleroma/ecto/ecto.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,49 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-onl
 | 
			
		||||
defmodule Mix.Tasks.Pleroma.Ecto do
 | 
			
		||||
  @doc """
 | 
			
		||||
  Ensures the given repository's migrations path exists on the file system.
 | 
			
		||||
  """
 | 
			
		||||
  @spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t()
 | 
			
		||||
  def ensure_migrations_path(repo, opts) do
 | 
			
		||||
    path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations")
 | 
			
		||||
 | 
			
		||||
    path =
 | 
			
		||||
      case Path.type(path) do
 | 
			
		||||
        :relative ->
 | 
			
		||||
          Path.join(Application.app_dir(:pleroma), path)
 | 
			
		||||
 | 
			
		||||
        :absolute ->
 | 
			
		||||
          path
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    if not File.dir?(path) do
 | 
			
		||||
      raise_missing_migrations(Path.relative_to_cwd(path), repo)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    path
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns the private repository path relative to the source.
 | 
			
		||||
  """
 | 
			
		||||
  def source_repo_priv(repo) do
 | 
			
		||||
    config = repo.config()
 | 
			
		||||
    priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}"
 | 
			
		||||
    Path.join(Application.app_dir(:pleroma), priv)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp raise_missing_migrations(path, repo) do
 | 
			
		||||
    raise("""
 | 
			
		||||
    Could not find migrations directory #{inspect(path)}
 | 
			
		||||
    for repo #{inspect(repo)}.
 | 
			
		||||
    This may be because you are in a new project and the
 | 
			
		||||
    migration directory has not been created yet. Creating an
 | 
			
		||||
    empty directory at the path above will fix this error.
 | 
			
		||||
    If you expected existing migrations to be found, please
 | 
			
		||||
    make sure your repository has been properly configured
 | 
			
		||||
    and the configured path exists.
 | 
			
		||||
    """)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										63
									
								
								lib/mix/tasks/pleroma/ecto/migrate.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								lib/mix/tasks/pleroma/ecto/migrate.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-onl
 | 
			
		||||
 | 
			
		||||
defmodule Mix.Tasks.Pleroma.Ecto.Migrate do
 | 
			
		||||
  use Mix.Task
 | 
			
		||||
  import Mix.Pleroma
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  @shortdoc "Wrapper on `ecto.migrate` task."
 | 
			
		||||
 | 
			
		||||
  @aliases [
 | 
			
		||||
    n: :step,
 | 
			
		||||
    v: :to
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  @switches [
 | 
			
		||||
    all: :boolean,
 | 
			
		||||
    step: :integer,
 | 
			
		||||
    to: :integer,
 | 
			
		||||
    quiet: :boolean,
 | 
			
		||||
    log_sql: :boolean,
 | 
			
		||||
    strict_version_order: :boolean,
 | 
			
		||||
    migrations_path: :string
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Changes `Logger` level to `:info` before start migration.
 | 
			
		||||
  Changes level back when migration ends.
 | 
			
		||||
 | 
			
		||||
  ## Start migration
 | 
			
		||||
 | 
			
		||||
      mix pleroma.ecto.migrate [OPTIONS]
 | 
			
		||||
 | 
			
		||||
  Options:
 | 
			
		||||
    - see https://hexdocs.pm/ecto/2.0.0/Mix.Tasks.Ecto.Migrate.html
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def run(args \\ []) do
 | 
			
		||||
    load_pleroma()
 | 
			
		||||
    {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
 | 
			
		||||
 | 
			
		||||
    opts =
 | 
			
		||||
      if opts[:to] || opts[:step] || opts[:all],
 | 
			
		||||
        do: opts,
 | 
			
		||||
        else: Keyword.put(opts, :all, true)
 | 
			
		||||
 | 
			
		||||
    opts =
 | 
			
		||||
      if opts[:quiet],
 | 
			
		||||
        do: Keyword.merge(opts, log: false, log_sql: false),
 | 
			
		||||
        else: opts
 | 
			
		||||
 | 
			
		||||
    path = Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, opts)
 | 
			
		||||
 | 
			
		||||
    level = Logger.level()
 | 
			
		||||
    Logger.configure(level: :info)
 | 
			
		||||
 | 
			
		||||
    {:ok, _, _} = Ecto.Migrator.with_repo(Pleroma.Repo, &Ecto.Migrator.run(&1, path, :up, opts))
 | 
			
		||||
 | 
			
		||||
    Logger.configure(level: level)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										67
									
								
								lib/mix/tasks/pleroma/ecto/rollback.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								lib/mix/tasks/pleroma/ecto/rollback.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,67 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-onl
 | 
			
		||||
 | 
			
		||||
defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
 | 
			
		||||
  use Mix.Task
 | 
			
		||||
  import Mix.Pleroma
 | 
			
		||||
  require Logger
 | 
			
		||||
  @shortdoc "Wrapper on `ecto.rollback` task"
 | 
			
		||||
 | 
			
		||||
  @aliases [
 | 
			
		||||
    n: :step,
 | 
			
		||||
    v: :to
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  @switches [
 | 
			
		||||
    all: :boolean,
 | 
			
		||||
    step: :integer,
 | 
			
		||||
    to: :integer,
 | 
			
		||||
    start: :boolean,
 | 
			
		||||
    quiet: :boolean,
 | 
			
		||||
    log_sql: :boolean,
 | 
			
		||||
    migrations_path: :string
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Changes `Logger` level to `:info` before start rollback.
 | 
			
		||||
  Changes level back when rollback ends.
 | 
			
		||||
 | 
			
		||||
  ## Start rollback
 | 
			
		||||
 | 
			
		||||
      mix pleroma.ecto.rollback
 | 
			
		||||
 | 
			
		||||
  Options:
 | 
			
		||||
    - see https://hexdocs.pm/ecto/2.0.0/Mix.Tasks.Ecto.Rollback.html
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def run(args \\ []) do
 | 
			
		||||
    load_pleroma()
 | 
			
		||||
    {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
 | 
			
		||||
 | 
			
		||||
    opts =
 | 
			
		||||
      if opts[:to] || opts[:step] || opts[:all],
 | 
			
		||||
        do: opts,
 | 
			
		||||
        else: Keyword.put(opts, :step, 1)
 | 
			
		||||
 | 
			
		||||
    opts =
 | 
			
		||||
      if opts[:quiet],
 | 
			
		||||
        do: Keyword.merge(opts, log: false, log_sql: false),
 | 
			
		||||
        else: opts
 | 
			
		||||
 | 
			
		||||
    path = Mix.Tasks.Pleroma.Ecto.ensure_migrations_path(Pleroma.Repo, opts)
 | 
			
		||||
 | 
			
		||||
    level = Logger.level()
 | 
			
		||||
    Logger.configure(level: :info)
 | 
			
		||||
 | 
			
		||||
    if Pleroma.Config.get(:env) == :test do
 | 
			
		||||
      Logger.info("Rollback succesfully")
 | 
			
		||||
    else
 | 
			
		||||
      {:ok, _, _} =
 | 
			
		||||
        Ecto.Migrator.with_repo(Pleroma.Repo, &Ecto.Migrator.run(&1, path, :down, opts))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    Logger.configure(level: level)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -55,15 +55,13 @@ defmodule Mix.Tasks.Pleroma.Emoji do
 | 
			
		|||
  are extracted).
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  @default_manifest Pleroma.Config.get!([:emoji, :default_manifest])
 | 
			
		||||
 | 
			
		||||
  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)
 | 
			
		||||
      fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest())
 | 
			
		||||
 | 
			
		||||
    Enum.each(manifest, fn {name, info} ->
 | 
			
		||||
      to_print = [
 | 
			
		||||
| 
						 | 
				
			
			@ -88,7 +86,7 @@ def run(["get-packs" | args]) do
 | 
			
		|||
 | 
			
		||||
    {options, pack_names, []} = parse_global_opts(args)
 | 
			
		||||
 | 
			
		||||
    manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest
 | 
			
		||||
    manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest()
 | 
			
		||||
 | 
			
		||||
    manifest = fetch_manifest(manifest_url)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -298,4 +296,6 @@ defp client do
 | 
			
		|||
 | 
			
		||||
    Tesla.client(middleware)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp default_manifest, do: Pleroma.Config.get!([:emoji, :default_manifest])
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
 | 
			
		||||
defmodule Mix.Tasks.Pleroma.Instance do
 | 
			
		||||
  use Mix.Task
 | 
			
		||||
  alias Mix.Tasks.Pleroma.Common
 | 
			
		||||
  import Mix.Pleroma
 | 
			
		||||
 | 
			
		||||
  @shortdoc "Manages Pleroma instance"
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +29,11 @@ defmodule Mix.Tasks.Pleroma.Instance do
 | 
			
		|||
  - `--dbname DBNAME` - the name of the database to use
 | 
			
		||||
  - `--dbuser DBUSER` - the user (aka role) to use for the database connection
 | 
			
		||||
  - `--dbpass DBPASS` - the password to use for the database connection
 | 
			
		||||
  - `--rum Y/N` - Whether to enable RUM indexes
 | 
			
		||||
  - `--indexable Y/N` - Allow/disallow indexing site by search engines
 | 
			
		||||
  - `--db-configurable Y/N` - Allow/disallow configuring instance from admin part
 | 
			
		||||
  - `--uploads-dir` - the directory uploads go in when using a local uploader
 | 
			
		||||
  - `--static-dir` - the directory custom public files should be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  def run(["gen" | rest]) do
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +52,11 @@ def run(["gen" | rest]) do
 | 
			
		|||
          dbname: :string,
 | 
			
		||||
          dbuser: :string,
 | 
			
		||||
          dbpass: :string,
 | 
			
		||||
          indexable: :string
 | 
			
		||||
          rum: :string,
 | 
			
		||||
          indexable: :string,
 | 
			
		||||
          db_configurable: :string,
 | 
			
		||||
          uploads_dir: :string,
 | 
			
		||||
          static_dir: :string
 | 
			
		||||
        ],
 | 
			
		||||
        aliases: [
 | 
			
		||||
          o: :output,
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +76,7 @@ def run(["gen" | rest]) do
 | 
			
		|||
    if proceed? do
 | 
			
		||||
      [domain, port | _] =
 | 
			
		||||
        String.split(
 | 
			
		||||
          Common.get_option(
 | 
			
		||||
          get_option(
 | 
			
		||||
            options,
 | 
			
		||||
            :domain,
 | 
			
		||||
            "What domain will your instance use? (e.g pleroma.soykaf.com)"
 | 
			
		||||
| 
						 | 
				
			
			@ -77,16 +85,16 @@ def run(["gen" | rest]) do
 | 
			
		|||
        ) ++ [443]
 | 
			
		||||
 | 
			
		||||
      name =
 | 
			
		||||
        Common.get_option(
 | 
			
		||||
        get_option(
 | 
			
		||||
          options,
 | 
			
		||||
          :instance_name,
 | 
			
		||||
          "What is the name of your instance? (e.g. Pleroma/Soykaf)"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      email = Common.get_option(options, :admin_email, "What is your admin email address?")
 | 
			
		||||
      email = get_option(options, :admin_email, "What is your admin email address?")
 | 
			
		||||
 | 
			
		||||
      notify_email =
 | 
			
		||||
        Common.get_option(
 | 
			
		||||
        get_option(
 | 
			
		||||
          options,
 | 
			
		||||
          :notify_email,
 | 
			
		||||
          "What email address do you want to use for sending email notifications?",
 | 
			
		||||
| 
						 | 
				
			
			@ -94,21 +102,27 @@ def run(["gen" | rest]) do
 | 
			
		|||
        )
 | 
			
		||||
 | 
			
		||||
      indexable =
 | 
			
		||||
        Common.get_option(
 | 
			
		||||
        get_option(
 | 
			
		||||
          options,
 | 
			
		||||
          :indexable,
 | 
			
		||||
          "Do you want search engines to index your site? (y/n)",
 | 
			
		||||
          "y"
 | 
			
		||||
        ) === "y"
 | 
			
		||||
 | 
			
		||||
      dbhost =
 | 
			
		||||
        Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
 | 
			
		||||
      db_configurable? =
 | 
			
		||||
        get_option(
 | 
			
		||||
          options,
 | 
			
		||||
          :db_configurable,
 | 
			
		||||
          "Do you want to store the configuration in the database (allows controlling it from admin-fe)? (y/n)",
 | 
			
		||||
          "y"
 | 
			
		||||
        ) === "y"
 | 
			
		||||
 | 
			
		||||
      dbname =
 | 
			
		||||
        Common.get_option(options, :dbname, "What is the name of your database?", "pleroma_dev")
 | 
			
		||||
      dbhost = get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
 | 
			
		||||
 | 
			
		||||
      dbname = get_option(options, :dbname, "What is the name of your database?", "pleroma_dev")
 | 
			
		||||
 | 
			
		||||
      dbuser =
 | 
			
		||||
        Common.get_option(
 | 
			
		||||
        get_option(
 | 
			
		||||
          options,
 | 
			
		||||
          :dbuser,
 | 
			
		||||
          "What is the user used to connect to your database?",
 | 
			
		||||
| 
						 | 
				
			
			@ -116,7 +130,7 @@ def run(["gen" | rest]) do
 | 
			
		|||
        )
 | 
			
		||||
 | 
			
		||||
      dbpass =
 | 
			
		||||
        Common.get_option(
 | 
			
		||||
        get_option(
 | 
			
		||||
          options,
 | 
			
		||||
          :dbpass,
 | 
			
		||||
          "What is the password used to connect to your database?",
 | 
			
		||||
| 
						 | 
				
			
			@ -124,13 +138,38 @@ def run(["gen" | rest]) do
 | 
			
		|||
          "autogenerated"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      rum_enabled =
 | 
			
		||||
        get_option(
 | 
			
		||||
          options,
 | 
			
		||||
          :rum,
 | 
			
		||||
          "Would you like to use RUM indices?",
 | 
			
		||||
          "n"
 | 
			
		||||
        ) === "y"
 | 
			
		||||
 | 
			
		||||
      uploads_dir =
 | 
			
		||||
        get_option(
 | 
			
		||||
          options,
 | 
			
		||||
          :upload_dir,
 | 
			
		||||
          "What directory should media uploads go in (when using the local uploader)?",
 | 
			
		||||
          Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      static_dir =
 | 
			
		||||
        get_option(
 | 
			
		||||
          options,
 | 
			
		||||
          :static_dir,
 | 
			
		||||
          "What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
 | 
			
		||||
          Pleroma.Config.get([:instance, :static_dir])
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
 | 
			
		||||
      signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
 | 
			
		||||
      {web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
 | 
			
		||||
      template_dir = Application.app_dir(:pleroma, "priv") <> "/templates"
 | 
			
		||||
 | 
			
		||||
      result_config =
 | 
			
		||||
        EEx.eval_file(
 | 
			
		||||
          "sample_config.eex" |> Path.expand(__DIR__),
 | 
			
		||||
          template_dir <> "/sample_config.eex",
 | 
			
		||||
          domain: domain,
 | 
			
		||||
          port: port,
 | 
			
		||||
          email: email,
 | 
			
		||||
| 
						 | 
				
			
			@ -140,46 +179,50 @@ def run(["gen" | rest]) do
 | 
			
		|||
          dbname: dbname,
 | 
			
		||||
          dbuser: dbuser,
 | 
			
		||||
          dbpass: dbpass,
 | 
			
		||||
          version: Pleroma.Mixfile.project() |> Keyword.get(:version),
 | 
			
		||||
          secret: secret,
 | 
			
		||||
          signing_salt: signing_salt,
 | 
			
		||||
          web_push_public_key: Base.url_encode64(web_push_public_key, padding: false),
 | 
			
		||||
          web_push_private_key: Base.url_encode64(web_push_private_key, padding: false)
 | 
			
		||||
          web_push_private_key: Base.url_encode64(web_push_private_key, padding: false),
 | 
			
		||||
          db_configurable?: db_configurable?,
 | 
			
		||||
          static_dir: static_dir,
 | 
			
		||||
          uploads_dir: uploads_dir,
 | 
			
		||||
          rum_enabled: rum_enabled
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      result_psql =
 | 
			
		||||
        EEx.eval_file(
 | 
			
		||||
          "sample_psql.eex" |> Path.expand(__DIR__),
 | 
			
		||||
          template_dir <> "/sample_psql.eex",
 | 
			
		||||
          dbname: dbname,
 | 
			
		||||
          dbuser: dbuser,
 | 
			
		||||
          dbpass: dbpass
 | 
			
		||||
          dbpass: dbpass,
 | 
			
		||||
          rum_enabled: rum_enabled
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      Mix.shell().info(
 | 
			
		||||
      shell_info(
 | 
			
		||||
        "Writing config to #{config_path}. You should rename it to config/prod.secret.exs or config/dev.secret.exs."
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      File.write(config_path, result_config)
 | 
			
		||||
      Mix.shell().info("Writing #{psql_path}.")
 | 
			
		||||
      shell_info("Writing #{psql_path}.")
 | 
			
		||||
      File.write(psql_path, result_psql)
 | 
			
		||||
 | 
			
		||||
      write_robots_txt(indexable)
 | 
			
		||||
      write_robots_txt(indexable, template_dir)
 | 
			
		||||
 | 
			
		||||
      Mix.shell().info(
 | 
			
		||||
      shell_info(
 | 
			
		||||
        "\n" <>
 | 
			
		||||
          """
 | 
			
		||||
          To get started:
 | 
			
		||||
          1. Verify the contents of the generated files.
 | 
			
		||||
          2. Run `sudo -u postgres psql -f #{Common.escape_sh_path(psql_path)}`.
 | 
			
		||||
          2. Run `sudo -u postgres psql -f #{escape_sh_path(psql_path)}`.
 | 
			
		||||
          """ <>
 | 
			
		||||
          if config_path in ["config/dev.secret.exs", "config/prod.secret.exs"] do
 | 
			
		||||
            ""
 | 
			
		||||
          else
 | 
			
		||||
            "3. Run `mv #{Common.escape_sh_path(config_path)} 'config/prod.secret.exs'`."
 | 
			
		||||
            "3. Run `mv #{escape_sh_path(config_path)} 'config/prod.secret.exs'`."
 | 
			
		||||
          end
 | 
			
		||||
      )
 | 
			
		||||
    else
 | 
			
		||||
      Mix.shell().error(
 | 
			
		||||
      shell_error(
 | 
			
		||||
        "The task would have overwritten the following files:\n" <>
 | 
			
		||||
          (Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <>
 | 
			
		||||
          "Rerun with `--force` to overwrite them."
 | 
			
		||||
| 
						 | 
				
			
			@ -187,10 +230,10 @@ def run(["gen" | rest]) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp write_robots_txt(indexable) do
 | 
			
		||||
  defp write_robots_txt(indexable, template_dir) do
 | 
			
		||||
    robots_txt =
 | 
			
		||||
      EEx.eval_file(
 | 
			
		||||
        Path.expand("robots_txt.eex", __DIR__),
 | 
			
		||||
        template_dir <> "/robots_txt.eex",
 | 
			
		||||
        indexable: indexable
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -204,10 +247,10 @@ defp write_robots_txt(indexable) do
 | 
			
		|||
 | 
			
		||||
    if File.exists?(robots_txt_path) do
 | 
			
		||||
      File.cp!(robots_txt_path, "#{robots_txt_path}.bak")
 | 
			
		||||
      Mix.shell().info("Backing up existing robots.txt to #{robots_txt_path}.bak")
 | 
			
		||||
      shell_info("Backing up existing robots.txt to #{robots_txt_path}.bak")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    File.write(robots_txt_path, robots_txt)
 | 
			
		||||
    Mix.shell().info("Writing #{robots_txt_path}.")
 | 
			
		||||
    shell_info("Writing #{robots_txt_path}.")
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
 | 
			
		||||
defmodule Mix.Tasks.Pleroma.Relay do
 | 
			
		||||
  use Mix.Task
 | 
			
		||||
  alias Mix.Tasks.Pleroma.Common
 | 
			
		||||
  import Mix.Pleroma
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.Relay
 | 
			
		||||
 | 
			
		||||
  @shortdoc "Manages remote relays"
 | 
			
		||||
| 
						 | 
				
			
			@ -24,24 +24,24 @@ defmodule Mix.Tasks.Pleroma.Relay do
 | 
			
		|||
  Example: ``mix pleroma.relay unfollow https://example.org/relay``
 | 
			
		||||
  """
 | 
			
		||||
  def run(["follow", target]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    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} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}")
 | 
			
		||||
      {:error, e} -> shell_error("Error while following #{target}: #{inspect(e)}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["unfollow", target]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    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} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}")
 | 
			
		||||
      {:error, e} -> shell_error("Error while following #{target}: #{inspect(e)}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
 | 
			
		||||
defmodule Mix.Tasks.Pleroma.Uploads do
 | 
			
		||||
  use Mix.Task
 | 
			
		||||
  alias Mix.Tasks.Pleroma.Common
 | 
			
		||||
  import Mix.Pleroma
 | 
			
		||||
  alias Pleroma.Upload
 | 
			
		||||
  alias Pleroma.Uploaders.Local
 | 
			
		||||
  require Logger
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +24,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do
 | 
			
		|||
  """
 | 
			
		||||
  def run(["migrate_local", target_uploader | args]) do
 | 
			
		||||
    delete? = Enum.member?(args, "--delete")
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
    local_path = Pleroma.Config.get!([Local, :uploads])
 | 
			
		||||
    uploader = Module.concat(Pleroma.Uploaders, target_uploader)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -38,10 +38,10 @@ def run(["migrate_local", target_uploader | args]) do
 | 
			
		|||
      Pleroma.Config.put([Upload, :uploader], uploader)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    Mix.shell().info("Migrating files from local #{local_path} to #{to_string(uploader)}")
 | 
			
		||||
    shell_info("Migrating files from local #{local_path} to #{to_string(uploader)}")
 | 
			
		||||
 | 
			
		||||
    if delete? do
 | 
			
		||||
      Mix.shell().info(
 | 
			
		||||
      shell_info(
 | 
			
		||||
        "Attention: uploaded files will be deleted, hope you have backups! (--delete ; cancel with ^C)"
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -78,7 +78,7 @@ def run(["migrate_local", target_uploader | args]) do
 | 
			
		|||
      |> Enum.filter(& &1)
 | 
			
		||||
 | 
			
		||||
    total_count = length(uploads)
 | 
			
		||||
    Mix.shell().info("Found #{total_count} uploads")
 | 
			
		||||
    shell_info("Found #{total_count} uploads")
 | 
			
		||||
 | 
			
		||||
    uploads
 | 
			
		||||
    |> Task.async_stream(
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +90,7 @@ def run(["migrate_local", target_uploader | args]) do
 | 
			
		|||
            :ok
 | 
			
		||||
 | 
			
		||||
          error ->
 | 
			
		||||
            Mix.shell().error("failed to upload #{inspect(upload.path)}: #{inspect(error)}")
 | 
			
		||||
            shell_error("failed to upload #{inspect(upload.path)}: #{inspect(error)}")
 | 
			
		||||
        end
 | 
			
		||||
      end,
 | 
			
		||||
      timeout: 150_000
 | 
			
		||||
| 
						 | 
				
			
			@ -99,10 +99,10 @@ def run(["migrate_local", target_uploader | args]) do
 | 
			
		|||
    # credo:disable-for-next-line Credo.Check.Warning.UnusedEnumOperation
 | 
			
		||||
    |> Enum.reduce(0, fn done, count ->
 | 
			
		||||
      count = count + length(done)
 | 
			
		||||
      Mix.shell().info("Uploaded #{count}/#{total_count} files")
 | 
			
		||||
      shell_info("Uploaded #{count}/#{total_count} files")
 | 
			
		||||
      count
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    Mix.shell().info("Done!")
 | 
			
		||||
    shell_info("Done!")
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,9 +5,10 @@
 | 
			
		|||
defmodule Mix.Tasks.Pleroma.User do
 | 
			
		||||
  use Mix.Task
 | 
			
		||||
  import Ecto.Changeset
 | 
			
		||||
  alias Mix.Tasks.Pleroma.Common
 | 
			
		||||
  import Mix.Pleroma
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.UserInviteToken
 | 
			
		||||
  alias Pleroma.Web.OAuth
 | 
			
		||||
 | 
			
		||||
  @shortdoc "Manages Pleroma users"
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +50,10 @@ defmodule Mix.Tasks.Pleroma.User do
 | 
			
		|||
 | 
			
		||||
      mix pleroma.user delete_activities NICKNAME
 | 
			
		||||
 | 
			
		||||
  ## Sign user out from all applications (delete user's OAuth tokens and authorizations).
 | 
			
		||||
 | 
			
		||||
      mix pleroma.user sign_out NICKNAME
 | 
			
		||||
 | 
			
		||||
  ## Deactivate or activate the user's account.
 | 
			
		||||
 | 
			
		||||
      mix pleroma.user toggle_activated NICKNAME
 | 
			
		||||
| 
						 | 
				
			
			@ -115,7 +120,7 @@ def run(["new", nickname, email | rest]) do
 | 
			
		|||
    admin? = Keyword.get(options, :admin, false)
 | 
			
		||||
    assume_yes? = Keyword.get(options, :assume_yes, false)
 | 
			
		||||
 | 
			
		||||
    Mix.shell().info("""
 | 
			
		||||
    shell_info("""
 | 
			
		||||
    A user will be created with the following information:
 | 
			
		||||
      - nickname: #{nickname}
 | 
			
		||||
      - email: #{email}
 | 
			
		||||
| 
						 | 
				
			
			@ -128,10 +133,10 @@ def run(["new", nickname, email | rest]) do
 | 
			
		|||
      - admin: #{if(admin?, do: "true", else: "false")}
 | 
			
		||||
    """)
 | 
			
		||||
 | 
			
		||||
    proceed? = assume_yes? or Mix.shell().yes?("Continue?")
 | 
			
		||||
    proceed? = assume_yes? or shell_yes?("Continue?")
 | 
			
		||||
 | 
			
		||||
    if proceed? do
 | 
			
		||||
      Common.start_pleroma()
 | 
			
		||||
      start_pleroma()
 | 
			
		||||
 | 
			
		||||
      params = %{
 | 
			
		||||
        nickname: nickname,
 | 
			
		||||
| 
						 | 
				
			
			@ -145,7 +150,7 @@ def run(["new", nickname, email | rest]) do
 | 
			
		|||
      changeset = User.register_changeset(%User{}, params, need_confirmation: false)
 | 
			
		||||
      {:ok, _user} = User.register(changeset)
 | 
			
		||||
 | 
			
		||||
      Mix.shell().info("User #{nickname} created")
 | 
			
		||||
      shell_info("User #{nickname} created")
 | 
			
		||||
 | 
			
		||||
      if moderator? do
 | 
			
		||||
        run(["set", nickname, "--moderator"])
 | 
			
		||||
| 
						 | 
				
			
			@ -159,43 +164,43 @@ def run(["new", nickname, email | rest]) do
 | 
			
		|||
        run(["reset_password", nickname])
 | 
			
		||||
      end
 | 
			
		||||
    else
 | 
			
		||||
      Mix.shell().info("User will not be created.")
 | 
			
		||||
      shell_info("User will not be created.")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["rm", nickname]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
 | 
			
		||||
      User.perform(:delete, user)
 | 
			
		||||
      Mix.shell().info("User #{nickname} deleted.")
 | 
			
		||||
      shell_info("User #{nickname} deleted.")
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        Mix.shell().error("No local user #{nickname}")
 | 
			
		||||
        shell_error("No local user #{nickname}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["toggle_activated", nickname]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    with %User{} = user <- User.get_cached_by_nickname(nickname) do
 | 
			
		||||
      {:ok, user} = User.deactivate(user, !user.info.deactivated)
 | 
			
		||||
 | 
			
		||||
      Mix.shell().info(
 | 
			
		||||
      shell_info(
 | 
			
		||||
        "Activation status of #{nickname}: #{if(user.info.deactivated, do: "de", else: "")}activated"
 | 
			
		||||
      )
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        Mix.shell().error("No user #{nickname}")
 | 
			
		||||
        shell_error("No user #{nickname}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["reset_password", nickname]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
 | 
			
		||||
         {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
 | 
			
		||||
      Mix.shell().info("Generated password reset token for #{user.nickname}")
 | 
			
		||||
      shell_info("Generated password reset token for #{user.nickname}")
 | 
			
		||||
 | 
			
		||||
      IO.puts(
 | 
			
		||||
        "URL: #{
 | 
			
		||||
| 
						 | 
				
			
			@ -208,15 +213,15 @@ def run(["reset_password", nickname]) do
 | 
			
		|||
      )
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        Mix.shell().error("No local user #{nickname}")
 | 
			
		||||
        shell_error("No local user #{nickname}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["unsubscribe", nickname]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    with %User{} = user <- User.get_cached_by_nickname(nickname) do
 | 
			
		||||
      Mix.shell().info("Deactivating #{user.nickname}")
 | 
			
		||||
      shell_info("Deactivating #{user.nickname}")
 | 
			
		||||
      User.deactivate(user)
 | 
			
		||||
 | 
			
		||||
      {:ok, friends} = User.get_friends(user)
 | 
			
		||||
| 
						 | 
				
			
			@ -224,7 +229,7 @@ def run(["unsubscribe", nickname]) do
 | 
			
		|||
      Enum.each(friends, fn friend ->
 | 
			
		||||
        user = User.get_cached_by_id(user.id)
 | 
			
		||||
 | 
			
		||||
        Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}")
 | 
			
		||||
        shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
 | 
			
		||||
        User.unfollow(user, friend)
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -233,16 +238,16 @@ def run(["unsubscribe", nickname]) do
 | 
			
		|||
      user = User.get_cached_by_id(user.id)
 | 
			
		||||
 | 
			
		||||
      if Enum.empty?(user.following) do
 | 
			
		||||
        Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}")
 | 
			
		||||
        shell_info("Successfully unsubscribed all followers from #{user.nickname}")
 | 
			
		||||
      end
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        Mix.shell().error("No user #{nickname}")
 | 
			
		||||
        shell_error("No user #{nickname}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["set", nickname | rest]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    {options, [], []} =
 | 
			
		||||
      OptionParser.parse(
 | 
			
		||||
| 
						 | 
				
			
			@ -274,33 +279,33 @@ def run(["set", nickname | rest]) do
 | 
			
		|||
        end
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        Mix.shell().error("No local user #{nickname}")
 | 
			
		||||
        shell_error("No local user #{nickname}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["tag", nickname | tags]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    with %User{} = user <- User.get_cached_by_nickname(nickname) do
 | 
			
		||||
      user = user |> User.tag(tags)
 | 
			
		||||
 | 
			
		||||
      Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
 | 
			
		||||
      shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        Mix.shell().error("Could not change user tags for #{nickname}")
 | 
			
		||||
        shell_error("Could not change user tags for #{nickname}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["untag", nickname | tags]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    with %User{} = user <- User.get_cached_by_nickname(nickname) do
 | 
			
		||||
      user = user |> User.untag(tags)
 | 
			
		||||
 | 
			
		||||
      Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
 | 
			
		||||
      shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        Mix.shell().error("Could not change user tags for #{nickname}")
 | 
			
		||||
        shell_error("Could not change user tags for #{nickname}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -321,14 +326,12 @@ def run(["invite" | rest]) do
 | 
			
		|||
      end)
 | 
			
		||||
      |> Enum.into(%{})
 | 
			
		||||
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    with {:ok, val} <- options[:expires_at],
 | 
			
		||||
         options = Map.put(options, :expires_at, val),
 | 
			
		||||
         {:ok, invite} <- UserInviteToken.create_invite(options) do
 | 
			
		||||
      Mix.shell().info(
 | 
			
		||||
        "Generated user invite token " <> String.replace(invite.invite_type, "_", " ")
 | 
			
		||||
      )
 | 
			
		||||
      shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " "))
 | 
			
		||||
 | 
			
		||||
      url =
 | 
			
		||||
        Pleroma.Web.Router.Helpers.redirect_url(
 | 
			
		||||
| 
						 | 
				
			
			@ -340,14 +343,14 @@ def run(["invite" | rest]) do
 | 
			
		|||
      IO.puts(url)
 | 
			
		||||
    else
 | 
			
		||||
      error ->
 | 
			
		||||
        Mix.shell().error("Could not create invite token: #{inspect(error)}")
 | 
			
		||||
        shell_error("Could not create invite token: #{inspect(error)}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["invites"]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    Mix.shell().info("Invites list:")
 | 
			
		||||
    shell_info("Invites list:")
 | 
			
		||||
 | 
			
		||||
    UserInviteToken.list_invites()
 | 
			
		||||
    |> Enum.each(fn invite ->
 | 
			
		||||
| 
						 | 
				
			
			@ -361,7 +364,7 @@ def run(["invites"]) do
 | 
			
		|||
          " | Max use: #{max_use}    Left use: #{max_use - invite.uses}"
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      Mix.shell().info(
 | 
			
		||||
      shell_info(
 | 
			
		||||
        "ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{
 | 
			
		||||
          invite.used
 | 
			
		||||
        }#{expire_info}#{using_info}"
 | 
			
		||||
| 
						 | 
				
			
			@ -370,40 +373,54 @@ def run(["invites"]) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["revoke_invite", token]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    with {:ok, invite} <- UserInviteToken.find_by_token(token),
 | 
			
		||||
         {:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do
 | 
			
		||||
      Mix.shell().info("Invite for token #{token} was revoked.")
 | 
			
		||||
      shell_info("Invite for token #{token} was revoked.")
 | 
			
		||||
    else
 | 
			
		||||
      _ -> Mix.shell().error("No invite found with token #{token}")
 | 
			
		||||
      _ -> shell_error("No invite found with token #{token}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["delete_activities", nickname]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    start_pleroma()
 | 
			
		||||
 | 
			
		||||
    with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
 | 
			
		||||
      {:ok, _} = User.delete_user_activities(user)
 | 
			
		||||
      Mix.shell().info("User #{nickname} statuses deleted.")
 | 
			
		||||
      shell_info("User #{nickname} statuses deleted.")
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        Mix.shell().error("No local user #{nickname}")
 | 
			
		||||
        shell_error("No local user #{nickname}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(["toggle_confirmed", nickname]) do
 | 
			
		||||
    Common.start_pleroma()
 | 
			
		||||
    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"
 | 
			
		||||
 | 
			
		||||
      Mix.shell().info("#{nickname} #{message} confirmation.")
 | 
			
		||||
      shell_info("#{nickname} #{message} confirmation.")
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        Mix.shell().error("No local user #{nickname}")
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -416,7 +433,7 @@ defp set_moderator(user, value) do
 | 
			
		|||
 | 
			
		||||
    {:ok, user} = User.update_and_set_cache(user_cng)
 | 
			
		||||
 | 
			
		||||
    Mix.shell().info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")
 | 
			
		||||
    shell_info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")
 | 
			
		||||
    user
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -429,7 +446,7 @@ defp set_admin(user, value) do
 | 
			
		|||
 | 
			
		||||
    {:ok, user} = User.update_and_set_cache(user_cng)
 | 
			
		||||
 | 
			
		||||
    Mix.shell().info("Admin status of #{user.nickname}: #{user.info.is_admin}")
 | 
			
		||||
    shell_info("Admin status of #{user.nickname}: #{user.info.is_admin}")
 | 
			
		||||
    user
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -442,7 +459,7 @@ defp set_locked(user, value) do
 | 
			
		|||
 | 
			
		||||
    {:ok, user} = User.update_and_set_cache(user_cng)
 | 
			
		||||
 | 
			
		||||
    Mix.shell().info("Locked status of #{user.nickname}: #{user.info.locked}")
 | 
			
		||||
    shell_info("Locked status of #{user.nickname}: #{user.info.locked}")
 | 
			
		||||
    user
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -343,4 +343,6 @@ def restrict_deactivated_users(query) do
 | 
			
		|||
        )
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defdelegate search(user, query), to: Pleroma.Activity.Search
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										81
									
								
								lib/pleroma/activity/search.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								lib/pleroma/activity/search.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +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.Activity.Search do
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.Object.Fetcher
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.Visibility
 | 
			
		||||
 | 
			
		||||
  import Ecto.Query
 | 
			
		||||
 | 
			
		||||
  def search(user, search_query) do
 | 
			
		||||
    index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin
 | 
			
		||||
 | 
			
		||||
    Activity
 | 
			
		||||
    |> Activity.with_preloaded_object()
 | 
			
		||||
    |> Activity.restrict_deactivated_users()
 | 
			
		||||
    |> restrict_public()
 | 
			
		||||
    |> query_with(index_type, search_query)
 | 
			
		||||
    |> maybe_restrict_local(user)
 | 
			
		||||
    |> Repo.all()
 | 
			
		||||
    |> maybe_fetch(user, search_query)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp restrict_public(q) do
 | 
			
		||||
    from([a, o] in q,
 | 
			
		||||
      where: fragment("?->>'type' = 'Create'", a.data),
 | 
			
		||||
      where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
 | 
			
		||||
      limit: 40
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp query_with(q, :gin, search_query) do
 | 
			
		||||
    from([a, o] in q,
 | 
			
		||||
      where:
 | 
			
		||||
        fragment(
 | 
			
		||||
          "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
 | 
			
		||||
          o.data,
 | 
			
		||||
          ^search_query
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp query_with(q, :rum, search_query) do
 | 
			
		||||
    from([a, o] in q,
 | 
			
		||||
      where:
 | 
			
		||||
        fragment(
 | 
			
		||||
          "? @@ plainto_tsquery('english', ?)",
 | 
			
		||||
          o.fts_content,
 | 
			
		||||
          ^search_query
 | 
			
		||||
        ),
 | 
			
		||||
      order_by: [fragment("? <=> now()::date", o.inserted_at)]
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_restrict_local(q, user) do
 | 
			
		||||
    limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
 | 
			
		||||
 | 
			
		||||
    case {limit, user} do
 | 
			
		||||
      {:all, _} -> restrict_local(q)
 | 
			
		||||
      {:unauthenticated, %User{}} -> q
 | 
			
		||||
      {:unauthenticated, _} -> restrict_local(q)
 | 
			
		||||
      {false, _} -> q
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp restrict_local(q), do: where(q, local: true)
 | 
			
		||||
 | 
			
		||||
  defp maybe_fetch(activities, user, search_query) do
 | 
			
		||||
    with true <- Regex.match?(~r/https?:/, search_query),
 | 
			
		||||
         {:ok, object} <- Fetcher.fetch_object_from_id(search_query),
 | 
			
		||||
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
 | 
			
		||||
         true <- Visibility.visible_for_user?(activity, user) do
 | 
			
		||||
      activities ++ [activity]
 | 
			
		||||
    else
 | 
			
		||||
      _ -> activities
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -4,7 +4,6 @@
 | 
			
		|||
 | 
			
		||||
defmodule Pleroma.Application do
 | 
			
		||||
  use Application
 | 
			
		||||
  import Supervisor.Spec
 | 
			
		||||
 | 
			
		||||
  @name Mix.Project.config()[:name]
 | 
			
		||||
  @version Mix.Project.config()[:version]
 | 
			
		||||
| 
						 | 
				
			
			@ -31,96 +30,128 @@ def start(_type, _args) do
 | 
			
		|||
    children =
 | 
			
		||||
      [
 | 
			
		||||
        # Start the Ecto repository
 | 
			
		||||
        supervisor(Pleroma.Repo, []),
 | 
			
		||||
        worker(Pleroma.Emoji, []),
 | 
			
		||||
        worker(Pleroma.Captcha, []),
 | 
			
		||||
        worker(
 | 
			
		||||
          Cachex,
 | 
			
		||||
          [
 | 
			
		||||
            :used_captcha_cache,
 | 
			
		||||
            [
 | 
			
		||||
              ttl_interval: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
 | 
			
		||||
            ]
 | 
			
		||||
          ],
 | 
			
		||||
          id: :cachex_used_captcha_cache
 | 
			
		||||
        ),
 | 
			
		||||
        worker(
 | 
			
		||||
          Cachex,
 | 
			
		||||
          [
 | 
			
		||||
            :user_cache,
 | 
			
		||||
            [
 | 
			
		||||
              default_ttl: 25_000,
 | 
			
		||||
              ttl_interval: 1000,
 | 
			
		||||
              limit: 2500
 | 
			
		||||
            ]
 | 
			
		||||
          ],
 | 
			
		||||
          id: :cachex_user
 | 
			
		||||
        ),
 | 
			
		||||
        worker(
 | 
			
		||||
          Cachex,
 | 
			
		||||
          [
 | 
			
		||||
            :object_cache,
 | 
			
		||||
            [
 | 
			
		||||
              default_ttl: 25_000,
 | 
			
		||||
              ttl_interval: 1000,
 | 
			
		||||
              limit: 2500
 | 
			
		||||
            ]
 | 
			
		||||
          ],
 | 
			
		||||
          id: :cachex_object
 | 
			
		||||
        ),
 | 
			
		||||
        worker(
 | 
			
		||||
          Cachex,
 | 
			
		||||
          [
 | 
			
		||||
            :rich_media_cache,
 | 
			
		||||
            [
 | 
			
		||||
              default_ttl: :timer.minutes(120),
 | 
			
		||||
              limit: 5000
 | 
			
		||||
            ]
 | 
			
		||||
          ],
 | 
			
		||||
          id: :cachex_rich_media
 | 
			
		||||
        ),
 | 
			
		||||
        worker(
 | 
			
		||||
          Cachex,
 | 
			
		||||
          [
 | 
			
		||||
            :scrubber_cache,
 | 
			
		||||
            [
 | 
			
		||||
              limit: 2500
 | 
			
		||||
            ]
 | 
			
		||||
          ],
 | 
			
		||||
          id: :cachex_scrubber
 | 
			
		||||
        ),
 | 
			
		||||
        worker(
 | 
			
		||||
          Cachex,
 | 
			
		||||
          [
 | 
			
		||||
            :idempotency_cache,
 | 
			
		||||
            [
 | 
			
		||||
              expiration:
 | 
			
		||||
                expiration(
 | 
			
		||||
                  default: :timer.seconds(6 * 60 * 60),
 | 
			
		||||
                  interval: :timer.seconds(60)
 | 
			
		||||
                ),
 | 
			
		||||
              limit: 2500
 | 
			
		||||
            ]
 | 
			
		||||
          ],
 | 
			
		||||
          id: :cachex_idem
 | 
			
		||||
        ),
 | 
			
		||||
        worker(Pleroma.FlakeId, []),
 | 
			
		||||
        worker(Pleroma.ScheduledActivityWorker, [])
 | 
			
		||||
        %{id: Pleroma.Repo, start: {Pleroma.Repo, :start_link, []}, type: :supervisor},
 | 
			
		||||
        %{id: Pleroma.Config.TransferTask, start: {Pleroma.Config.TransferTask, :start_link, []}},
 | 
			
		||||
        %{id: Pleroma.Emoji, start: {Pleroma.Emoji, :start_link, []}},
 | 
			
		||||
        %{id: Pleroma.Captcha, start: {Pleroma.Captcha, :start_link, []}},
 | 
			
		||||
        %{
 | 
			
		||||
          id: :cachex_used_captcha_cache,
 | 
			
		||||
          start:
 | 
			
		||||
            {Cachex, :start_link,
 | 
			
		||||
             [
 | 
			
		||||
               :used_captcha_cache,
 | 
			
		||||
               [
 | 
			
		||||
                 ttl_interval:
 | 
			
		||||
                   :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
 | 
			
		||||
               ]
 | 
			
		||||
             ]}
 | 
			
		||||
        },
 | 
			
		||||
        %{
 | 
			
		||||
          id: :cachex_user,
 | 
			
		||||
          start:
 | 
			
		||||
            {Cachex, :start_link,
 | 
			
		||||
             [
 | 
			
		||||
               :user_cache,
 | 
			
		||||
               [
 | 
			
		||||
                 default_ttl: 25_000,
 | 
			
		||||
                 ttl_interval: 1000,
 | 
			
		||||
                 limit: 2500
 | 
			
		||||
               ]
 | 
			
		||||
             ]}
 | 
			
		||||
        },
 | 
			
		||||
        %{
 | 
			
		||||
          id: :cachex_object,
 | 
			
		||||
          start:
 | 
			
		||||
            {Cachex, :start_link,
 | 
			
		||||
             [
 | 
			
		||||
               :object_cache,
 | 
			
		||||
               [
 | 
			
		||||
                 default_ttl: 25_000,
 | 
			
		||||
                 ttl_interval: 1000,
 | 
			
		||||
                 limit: 2500
 | 
			
		||||
               ]
 | 
			
		||||
             ]}
 | 
			
		||||
        },
 | 
			
		||||
        %{
 | 
			
		||||
          id: :cachex_rich_media,
 | 
			
		||||
          start:
 | 
			
		||||
            {Cachex, :start_link,
 | 
			
		||||
             [
 | 
			
		||||
               :rich_media_cache,
 | 
			
		||||
               [
 | 
			
		||||
                 default_ttl: :timer.minutes(120),
 | 
			
		||||
                 limit: 5000
 | 
			
		||||
               ]
 | 
			
		||||
             ]}
 | 
			
		||||
        },
 | 
			
		||||
        %{
 | 
			
		||||
          id: :cachex_scrubber,
 | 
			
		||||
          start:
 | 
			
		||||
            {Cachex, :start_link,
 | 
			
		||||
             [
 | 
			
		||||
               :scrubber_cache,
 | 
			
		||||
               [
 | 
			
		||||
                 limit: 2500
 | 
			
		||||
               ]
 | 
			
		||||
             ]}
 | 
			
		||||
        },
 | 
			
		||||
        %{
 | 
			
		||||
          id: :cachex_idem,
 | 
			
		||||
          start:
 | 
			
		||||
            {Cachex, :start_link,
 | 
			
		||||
             [
 | 
			
		||||
               :idempotency_cache,
 | 
			
		||||
               [
 | 
			
		||||
                 expiration:
 | 
			
		||||
                   expiration(
 | 
			
		||||
                     default: :timer.seconds(6 * 60 * 60),
 | 
			
		||||
                     interval: :timer.seconds(60)
 | 
			
		||||
                   ),
 | 
			
		||||
                 limit: 2500
 | 
			
		||||
               ]
 | 
			
		||||
             ]}
 | 
			
		||||
        },
 | 
			
		||||
        %{id: Pleroma.FlakeId, start: {Pleroma.FlakeId, :start_link, []}},
 | 
			
		||||
        %{
 | 
			
		||||
          id: Pleroma.ScheduledActivityWorker,
 | 
			
		||||
          start: {Pleroma.ScheduledActivityWorker, :start_link, []}
 | 
			
		||||
        }
 | 
			
		||||
      ] ++
 | 
			
		||||
        hackney_pool_children() ++
 | 
			
		||||
        [
 | 
			
		||||
          worker(Pleroma.Web.Federator.RetryQueue, []),
 | 
			
		||||
          worker(Pleroma.Web.OAuth.Token.CleanWorker, []),
 | 
			
		||||
          worker(Pleroma.Stats, []),
 | 
			
		||||
          worker(Task, [&Pleroma.Web.Push.init/0], restart: :temporary, id: :web_push_init),
 | 
			
		||||
          worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary, id: :federator_init)
 | 
			
		||||
          %{
 | 
			
		||||
            id: Pleroma.Web.Federator.RetryQueue,
 | 
			
		||||
            start: {Pleroma.Web.Federator.RetryQueue, :start_link, []}
 | 
			
		||||
          },
 | 
			
		||||
          %{
 | 
			
		||||
            id: Pleroma.Web.OAuth.Token.CleanWorker,
 | 
			
		||||
            start: {Pleroma.Web.OAuth.Token.CleanWorker, :start_link, []}
 | 
			
		||||
          },
 | 
			
		||||
          %{
 | 
			
		||||
            id: Pleroma.Stats,
 | 
			
		||||
            start: {Pleroma.Stats, :start_link, []}
 | 
			
		||||
          },
 | 
			
		||||
          %{
 | 
			
		||||
            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
 | 
			
		||||
          }
 | 
			
		||||
        ] ++
 | 
			
		||||
        streamer_child() ++
 | 
			
		||||
        chat_child() ++
 | 
			
		||||
        [
 | 
			
		||||
          # Start the endpoint when the application starts
 | 
			
		||||
          supervisor(Pleroma.Web.Endpoint, []),
 | 
			
		||||
          worker(Pleroma.Gopher.Server, [])
 | 
			
		||||
          %{
 | 
			
		||||
            id: Pleroma.Web.Endpoint,
 | 
			
		||||
            start: {Pleroma.Web.Endpoint, :start_link, []},
 | 
			
		||||
            type: :supervisor
 | 
			
		||||
          },
 | 
			
		||||
          %{id: Pleroma.Gopher.Server, start: {Pleroma.Gopher.Server, :start_link, []}}
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
 | 
			
		||||
| 
						 | 
				
			
			@ -144,7 +175,6 @@ defp setup_instrumenters do
 | 
			
		|||
      Pleroma.Repo.Instrumenter.setup()
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    Prometheus.Registry.register_collector(:prometheus_process_collector)
 | 
			
		||||
    Pleroma.Web.Endpoint.MetricsExporter.setup()
 | 
			
		||||
    Pleroma.Web.Endpoint.PipelineInstrumenter.setup()
 | 
			
		||||
    Pleroma.Web.Endpoint.Instrumenter.setup()
 | 
			
		||||
| 
						 | 
				
			
			@ -157,24 +187,29 @@ def enabled_hackney_pools do
 | 
			
		|||
      else
 | 
			
		||||
        []
 | 
			
		||||
      end ++
 | 
			
		||||
      if Pleroma.Config.get([Pleroma.Uploader, :proxy_remote]) do
 | 
			
		||||
      if Pleroma.Config.get([Pleroma.Upload, :proxy_remote]) do
 | 
			
		||||
        [:upload]
 | 
			
		||||
      else
 | 
			
		||||
        []
 | 
			
		||||
      end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  if Mix.env() == :test do
 | 
			
		||||
  if Pleroma.Config.get(:env) == :test do
 | 
			
		||||
    defp streamer_child, do: []
 | 
			
		||||
    defp chat_child, do: []
 | 
			
		||||
  else
 | 
			
		||||
    defp streamer_child do
 | 
			
		||||
      [worker(Pleroma.Web.Streamer, [])]
 | 
			
		||||
      [%{id: Pleroma.Web.Streamer, start: {Pleroma.Web.Streamer, :start_link, []}}]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    defp chat_child do
 | 
			
		||||
      if Pleroma.Config.get([:chat, :enabled]) do
 | 
			
		||||
        [worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
 | 
			
		||||
        [
 | 
			
		||||
          %{
 | 
			
		||||
            id: Pleroma.Web.ChatChannel.ChatChannelState,
 | 
			
		||||
            start: {Pleroma.Web.ChatChannel.ChatChannelState, :start_link, []}
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      else
 | 
			
		||||
        []
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										42
									
								
								lib/pleroma/config/transfer_task.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								lib/pleroma/config/transfer_task.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
defmodule Pleroma.Config.TransferTask do
 | 
			
		||||
  use Task
 | 
			
		||||
  alias Pleroma.Web.AdminAPI.Config
 | 
			
		||||
 | 
			
		||||
  def start_link do
 | 
			
		||||
    load_and_update_env()
 | 
			
		||||
    if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Pleroma.Repo)
 | 
			
		||||
    :ignore
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def load_and_update_env do
 | 
			
		||||
    if Pleroma.Config.get([:instance, :dynamic_configuration]) and
 | 
			
		||||
         Ecto.Adapters.SQL.table_exists?(Pleroma.Repo, "config") do
 | 
			
		||||
      Pleroma.Repo.all(Config)
 | 
			
		||||
      |> Enum.each(&update_env(&1))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp update_env(setting) do
 | 
			
		||||
    try do
 | 
			
		||||
      key =
 | 
			
		||||
        if String.starts_with?(setting.key, "Pleroma.") do
 | 
			
		||||
          "Elixir." <> setting.key
 | 
			
		||||
        else
 | 
			
		||||
          setting.key
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      Application.put_env(
 | 
			
		||||
        :pleroma,
 | 
			
		||||
        String.to_existing_atom(key),
 | 
			
		||||
        Config.from_binary(setting.value)
 | 
			
		||||
      )
 | 
			
		||||
    rescue
 | 
			
		||||
      e ->
 | 
			
		||||
        require Logger
 | 
			
		||||
 | 
			
		||||
        Logger.warn(
 | 
			
		||||
          "updating env causes error, key: #{inspect(setting.key)}, error: #{inspect(e)}"
 | 
			
		||||
        )
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +49,7 @@ 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),
 | 
			
		||||
         "Note" <- object.data["type"],
 | 
			
		||||
         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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,10 +59,10 @@ def mark_as_unread(participation) do
 | 
			
		|||
  def for_user(user, params \\ %{}) do
 | 
			
		||||
    from(p in __MODULE__,
 | 
			
		||||
      where: p.user_id == ^user.id,
 | 
			
		||||
      order_by: [desc: p.updated_at]
 | 
			
		||||
      order_by: [desc: p.updated_at],
 | 
			
		||||
      preload: [conversation: [:users]]
 | 
			
		||||
    )
 | 
			
		||||
    |> Pleroma.Pagination.fetch_paginated(params)
 | 
			
		||||
    |> Repo.preload(conversation: [:users])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def for_user_with_last_activity_id(user, params \\ %{}) do
 | 
			
		||||
| 
						 | 
				
			
			@ -79,5 +79,6 @@ def for_user_with_last_activity_id(user, params \\ %{}) do
 | 
			
		|||
        | last_activity_id: activity_id
 | 
			
		||||
      }
 | 
			
		||||
    end)
 | 
			
		||||
    |> Enum.filter(& &1.last_activity_id)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,6 @@ defmodule Pleroma.Emoji do
 | 
			
		|||
 | 
			
		||||
  @ets __MODULE__.Ets
 | 
			
		||||
  @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
 | 
			
		||||
  @groups Application.get_env(:pleroma, :emoji)[:groups]
 | 
			
		||||
 | 
			
		||||
  @doc false
 | 
			
		||||
  def start_link do
 | 
			
		||||
| 
						 | 
				
			
			@ -87,6 +86,8 @@ defp load do
 | 
			
		|||
        "emoji"
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    emoji_groups = Pleroma.Config.get([:emoji, :groups])
 | 
			
		||||
 | 
			
		||||
    case File.ls(emoji_dir_path) do
 | 
			
		||||
      {:error, :enoent} ->
 | 
			
		||||
        # The custom emoji directory doesn't exist,
 | 
			
		||||
| 
						 | 
				
			
			@ -97,14 +98,28 @@ defp load do
 | 
			
		|||
        # There was some other error
 | 
			
		||||
        Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
 | 
			
		||||
 | 
			
		||||
      {:ok, packs} ->
 | 
			
		||||
      {:ok, results} ->
 | 
			
		||||
        grouped =
 | 
			
		||||
          Enum.group_by(results, fn file -> File.dir?(Path.join(emoji_dir_path, file)) end)
 | 
			
		||||
 | 
			
		||||
        packs = grouped[true] || []
 | 
			
		||||
        files = grouped[false] || []
 | 
			
		||||
 | 
			
		||||
        # Print the packs we've found
 | 
			
		||||
        Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
 | 
			
		||||
 | 
			
		||||
        if not Enum.empty?(files) do
 | 
			
		||||
          Logger.warn(
 | 
			
		||||
            "Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{
 | 
			
		||||
              Enum.join(files, ", ")
 | 
			
		||||
            }"
 | 
			
		||||
          )
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        emojis =
 | 
			
		||||
          Enum.flat_map(
 | 
			
		||||
            packs,
 | 
			
		||||
            fn pack -> load_pack(Path.join(emoji_dir_path, pack)) end
 | 
			
		||||
            fn pack -> load_pack(Path.join(emoji_dir_path, pack), emoji_groups) end
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
        true = :ets.insert(@ets, emojis)
 | 
			
		||||
| 
						 | 
				
			
			@ -112,12 +127,12 @@ defp load do
 | 
			
		|||
 | 
			
		||||
    # Compat thing for old custom emoji handling & default emoji,
 | 
			
		||||
    # it should run even if there are no emoji packs
 | 
			
		||||
    shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || []
 | 
			
		||||
    shortcode_globs = Pleroma.Config.get([:emoji, :shortcode_globs], [])
 | 
			
		||||
 | 
			
		||||
    emojis =
 | 
			
		||||
      (load_from_file("config/emoji.txt") ++
 | 
			
		||||
         load_from_file("config/custom_emoji.txt") ++
 | 
			
		||||
         load_from_globs(shortcode_globs))
 | 
			
		||||
      (load_from_file("config/emoji.txt", emoji_groups) ++
 | 
			
		||||
         load_from_file("config/custom_emoji.txt", emoji_groups) ++
 | 
			
		||||
         load_from_globs(shortcode_globs, emoji_groups))
 | 
			
		||||
      |> Enum.reject(fn value -> value == nil end)
 | 
			
		||||
 | 
			
		||||
    true = :ets.insert(@ets, emojis)
 | 
			
		||||
| 
						 | 
				
			
			@ -125,13 +140,13 @@ defp load do
 | 
			
		|||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp load_pack(pack_dir) do
 | 
			
		||||
  defp load_pack(pack_dir, emoji_groups) do
 | 
			
		||||
    pack_name = Path.basename(pack_dir)
 | 
			
		||||
 | 
			
		||||
    emoji_txt = Path.join(pack_dir, "emoji.txt")
 | 
			
		||||
 | 
			
		||||
    if File.exists?(emoji_txt) do
 | 
			
		||||
      load_from_file(emoji_txt)
 | 
			
		||||
      load_from_file(emoji_txt, emoji_groups)
 | 
			
		||||
    else
 | 
			
		||||
      Logger.info(
 | 
			
		||||
        "No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji"
 | 
			
		||||
| 
						 | 
				
			
			@ -141,7 +156,7 @@ defp load_pack(pack_dir) do
 | 
			
		|||
      |> Enum.map(fn {shortcode, rel_file} ->
 | 
			
		||||
        filename = Path.join("/emoji/#{pack_name}", rel_file)
 | 
			
		||||
 | 
			
		||||
        {shortcode, filename, [to_string(match_extra(@groups, filename))]}
 | 
			
		||||
        {shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
 | 
			
		||||
      end)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -170,21 +185,21 @@ def find_all_emoji(dir, exts) do
 | 
			
		|||
    |> Enum.filter(fn f -> Path.extname(f) in exts end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp load_from_file(file) do
 | 
			
		||||
  defp load_from_file(file, emoji_groups) do
 | 
			
		||||
    if File.exists?(file) do
 | 
			
		||||
      load_from_file_stream(File.stream!(file))
 | 
			
		||||
      load_from_file_stream(File.stream!(file), emoji_groups)
 | 
			
		||||
    else
 | 
			
		||||
      []
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp load_from_file_stream(stream) do
 | 
			
		||||
  defp load_from_file_stream(stream, emoji_groups) do
 | 
			
		||||
    stream
 | 
			
		||||
    |> Stream.map(&String.trim/1)
 | 
			
		||||
    |> Stream.map(fn line ->
 | 
			
		||||
      case String.split(line, ~r/,\s*/) do
 | 
			
		||||
        [name, file] ->
 | 
			
		||||
          {name, file, [to_string(match_extra(@groups, file))]}
 | 
			
		||||
          {name, file, [to_string(match_extra(emoji_groups, file))]}
 | 
			
		||||
 | 
			
		||||
        [name, file | tags] ->
 | 
			
		||||
          {name, file, tags}
 | 
			
		||||
| 
						 | 
				
			
			@ -196,7 +211,7 @@ defp load_from_file_stream(stream) do
 | 
			
		|||
    |> Enum.to_list()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp load_from_globs(globs) do
 | 
			
		||||
  defp load_from_globs(globs, emoji_groups) do
 | 
			
		||||
    static_path = Path.join(:code.priv_dir(:pleroma), "static")
 | 
			
		||||
 | 
			
		||||
    paths =
 | 
			
		||||
| 
						 | 
				
			
			@ -207,7 +222,7 @@ defp load_from_globs(globs) do
 | 
			
		|||
      |> Enum.concat()
 | 
			
		||||
 | 
			
		||||
    Enum.map(paths, fn path ->
 | 
			
		||||
      tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))
 | 
			
		||||
      tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))
 | 
			
		||||
      shortcode = Path.basename(path, Path.extname(path))
 | 
			
		||||
      external_path = Path.join("/", Path.relative_to(path, static_path))
 | 
			
		||||
      {shortcode, external_path, [to_string(tag)]}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ defmodule Pleroma.Formatter do
 | 
			
		|||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Web.MediaProxy
 | 
			
		||||
 | 
			
		||||
  @safe_mention_regex ~r/^(\s*(?<mentions>@.+?\s+)+)(?<rest>.*)/s
 | 
			
		||||
  @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/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										27
									
								
								lib/pleroma/helpers/uri_helper.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/pleroma/helpers/uri_helper.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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.Helpers.UriHelper do
 | 
			
		||||
  def append_uri_params(uri, appended_params) do
 | 
			
		||||
    uri = URI.parse(uri)
 | 
			
		||||
    appended_params = for {k, v} <- appended_params, into: %{}, do: {to_string(k), v}
 | 
			
		||||
    existing_params = URI.query_decoder(uri.query || "") |> Enum.into(%{})
 | 
			
		||||
    updated_params_keys = Enum.uniq(Map.keys(existing_params) ++ Map.keys(appended_params))
 | 
			
		||||
 | 
			
		||||
    updated_params =
 | 
			
		||||
      for k <- updated_params_keys, do: {k, appended_params[k] || existing_params[k]}
 | 
			
		||||
 | 
			
		||||
    uri
 | 
			
		||||
    |> Map.put(:query, URI.encode_query(updated_params))
 | 
			
		||||
    |> URI.to_string()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def append_param_if_present(%{} = params, param_name, param_value) do
 | 
			
		||||
    if param_value do
 | 
			
		||||
      Map.put(params, param_name, param_value)
 | 
			
		||||
    else
 | 
			
		||||
      params
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +89,7 @@ def extract_first_external_url(object, content) do
 | 
			
		|||
    Cachex.fetch!(:scrubber_cache, key, fn _key ->
 | 
			
		||||
      result =
 | 
			
		||||
        content
 | 
			
		||||
        |> Floki.filter_out("a.mention")
 | 
			
		||||
        |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]")
 | 
			
		||||
        |> Floki.attribute("a", "href")
 | 
			
		||||
        |> Enum.at(0)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +104,6 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
 | 
			
		|||
  paragraphs, breaks and links are allowed through the filter.
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  @markup Application.get_env(:pleroma, :markup)
 | 
			
		||||
  @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
 | 
			
		||||
 | 
			
		||||
  require HtmlSanitizeEx.Scrubber.Meta
 | 
			
		||||
| 
						 | 
				
			
			@ -142,9 +141,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
 | 
			
		|||
  Meta.allow_tag_with_these_attributes("span", [])
 | 
			
		||||
 | 
			
		||||
  # allow inline images for custom emoji
 | 
			
		||||
  @allow_inline_images Keyword.get(@markup, :allow_inline_images)
 | 
			
		||||
 | 
			
		||||
  if @allow_inline_images do
 | 
			
		||||
  if Pleroma.Config.get([:markup, :allow_inline_images]) do
 | 
			
		||||
    # restrict img tags to http/https only, because of MediaProxy.
 | 
			
		||||
    Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -168,7 +165,6 @@ defmodule Pleroma.HTML.Scrubber.Default do
 | 
			
		|||
  # credo:disable-for-previous-line
 | 
			
		||||
  # No idea how to fix this one…
 | 
			
		||||
 | 
			
		||||
  @markup Application.get_env(:pleroma, :markup)
 | 
			
		||||
  @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
 | 
			
		||||
 | 
			
		||||
  Meta.remove_cdata_sections_before_scrub()
 | 
			
		||||
| 
						 | 
				
			
			@ -213,7 +209,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
 | 
			
		|||
  Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
 | 
			
		||||
  Meta.allow_tag_with_these_attributes("span", [])
 | 
			
		||||
 | 
			
		||||
  @allow_inline_images Keyword.get(@markup, :allow_inline_images)
 | 
			
		||||
  @allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images])
 | 
			
		||||
 | 
			
		||||
  if @allow_inline_images do
 | 
			
		||||
    # restrict img tags to http/https only, because of MediaProxy.
 | 
			
		||||
| 
						 | 
				
			
			@ -228,9 +224,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
 | 
			
		|||
    ])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @allow_tables Keyword.get(@markup, :allow_tables)
 | 
			
		||||
 | 
			
		||||
  if @allow_tables do
 | 
			
		||||
  if Pleroma.Config.get([:markup, :allow_tables]) do
 | 
			
		||||
    Meta.allow_tag_with_these_attributes("table", [])
 | 
			
		||||
    Meta.allow_tag_with_these_attributes("tbody", [])
 | 
			
		||||
    Meta.allow_tag_with_these_attributes("td", [])
 | 
			
		||||
| 
						 | 
				
			
			@ -239,9 +233,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
 | 
			
		|||
    Meta.allow_tag_with_these_attributes("tr", [])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @allow_headings Keyword.get(@markup, :allow_headings)
 | 
			
		||||
 | 
			
		||||
  if @allow_headings do
 | 
			
		||||
  if Pleroma.Config.get([:markup, :allow_headings]) do
 | 
			
		||||
    Meta.allow_tag_with_these_attributes("h1", [])
 | 
			
		||||
    Meta.allow_tag_with_these_attributes("h2", [])
 | 
			
		||||
    Meta.allow_tag_with_these_attributes("h3", [])
 | 
			
		||||
| 
						 | 
				
			
			@ -249,9 +241,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
 | 
			
		|||
    Meta.allow_tag_with_these_attributes("h5", [])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @allow_fonts Keyword.get(@markup, :allow_fonts)
 | 
			
		||||
 | 
			
		||||
  if @allow_fonts do
 | 
			
		||||
  if Pleroma.Config.get([:markup, :allow_fonts]) do
 | 
			
		||||
    Meta.allow_tag_with_these_attributes("font", ["face"])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,9 +32,11 @@ def new(opts \\ []) do
 | 
			
		|||
  defp hackney_options(opts) do
 | 
			
		||||
    options = Keyword.get(opts, :adapter, [])
 | 
			
		||||
    adapter_options = Pleroma.Config.get([:http, :adapter], [])
 | 
			
		||||
    proxy_url = Pleroma.Config.get([:http, :proxy_url], nil)
 | 
			
		||||
 | 
			
		||||
    @hackney_options
 | 
			
		||||
    |> Keyword.merge(adapter_options)
 | 
			
		||||
    |> Keyword.merge(options)
 | 
			
		||||
    |> Keyword.merge(proxy: proxy_url)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,12 +65,9 @@ defp process_sni_options(options, url) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def process_request_options(options) do
 | 
			
		||||
    config = Application.get_env(:pleroma, :http, [])
 | 
			
		||||
    proxy = Keyword.get(config, :proxy_url, nil)
 | 
			
		||||
 | 
			
		||||
    case proxy do
 | 
			
		||||
    case Pleroma.Config.get([:http, :proxy_url]) do
 | 
			
		||||
      nil -> options
 | 
			
		||||
      _ -> options ++ [proxy: proxy]
 | 
			
		||||
      proxy -> options ++ [proxy: proxy]
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ def set_consistently_unreachable(url_or_host),
 | 
			
		|||
 | 
			
		||||
  def reachability_datetime_threshold do
 | 
			
		||||
    federation_reachability_timeout_days =
 | 
			
		||||
      Pleroma.Config.get(:instance)[:federation_reachability_timeout_days] || 0
 | 
			
		||||
      Pleroma.Config.get([:instance, :federation_reachability_timeout_days], 0)
 | 
			
		||||
 | 
			
		||||
    if federation_reachability_timeout_days > 0 do
 | 
			
		||||
      NaiveDateTime.add(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,8 @@ defmodule Pleroma.Notification do
 | 
			
		|||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Web.CommonAPI
 | 
			
		||||
  alias Pleroma.Web.CommonAPI.Utils
 | 
			
		||||
  alias Pleroma.Web.Push
 | 
			
		||||
  alias Pleroma.Web.Streamer
 | 
			
		||||
 | 
			
		||||
  import Ecto.Query
 | 
			
		||||
  import Ecto.Changeset
 | 
			
		||||
| 
						 | 
				
			
			@ -125,10 +127,21 @@ def dismiss(%{id: user_id} = _user, id) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
 | 
			
		||||
      when type in ["Create", "Like", "Announce", "Follow"] do
 | 
			
		||||
    users = get_notified_from_activity(activity)
 | 
			
		||||
  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
 | 
			
		||||
| 
						 | 
				
			
			@ -140,8 +153,9 @@ 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)
 | 
			
		||||
      Pleroma.Web.Streamer.stream("user", notification)
 | 
			
		||||
      Pleroma.Web.Push.send(notification)
 | 
			
		||||
      Streamer.stream("user", notification)
 | 
			
		||||
      Streamer.stream("user:notification", notification)
 | 
			
		||||
      Push.send(notification)
 | 
			
		||||
      notification
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -166,7 +180,16 @@ def get_notified_from_activity(
 | 
			
		|||
  def get_notified_from_activity(_, _local_only), do: []
 | 
			
		||||
 | 
			
		||||
  def skip?(activity, user) do
 | 
			
		||||
    [:self, :blocked, :local, :muted, :followers, :follows, :recently_followed]
 | 
			
		||||
    [
 | 
			
		||||
      :self,
 | 
			
		||||
      :blocked,
 | 
			
		||||
      :muted,
 | 
			
		||||
      :followers,
 | 
			
		||||
      :follows,
 | 
			
		||||
      :non_followers,
 | 
			
		||||
      :non_follows,
 | 
			
		||||
      :recently_followed
 | 
			
		||||
    ]
 | 
			
		||||
    |> Enum.any?(&skip?(&1, activity, user))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -179,12 +202,6 @@ def skip?(:blocked, activity, user) do
 | 
			
		|||
    User.blocks?(user, %{ap_id: actor})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def skip?(:local, %{local: true}, %{info: %{notification_settings: %{"local" => false}}}),
 | 
			
		||||
    do: true
 | 
			
		||||
 | 
			
		||||
  def skip?(:local, %{local: false}, %{info: %{notification_settings: %{"remote" => false}}}),
 | 
			
		||||
    do: true
 | 
			
		||||
 | 
			
		||||
  def skip?(:muted, activity, user) do
 | 
			
		||||
    actor = activity.data["actor"]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -201,12 +218,32 @@ def skip?(
 | 
			
		|||
    User.following?(follower, user)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def skip?(
 | 
			
		||||
        :non_followers,
 | 
			
		||||
        activity,
 | 
			
		||||
        %{info: %{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
 | 
			
		||||
    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
 | 
			
		||||
      ) 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"]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,9 @@ def change(struct, params \\ %{}) do
 | 
			
		|||
    |> 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_ap_id(nil), do: nil
 | 
			
		||||
 | 
			
		||||
  def get_by_ap_id(ap_id) do
 | 
			
		||||
| 
						 | 
				
			
			@ -195,4 +198,34 @@ def decrease_replies_count(ap_id) do
 | 
			
		|||
      _ -> {: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
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,7 @@
 | 
			
		|||
# 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
defmodule Pleroma.Object.Fetcher do
 | 
			
		||||
  alias Pleroma.HTTP
 | 
			
		||||
  alias Pleroma.Object
 | 
			
		||||
  alias Pleroma.Object.Containment
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.Transmogrifier
 | 
			
		||||
| 
						 | 
				
			
			@ -6,8 +7,6 @@ defmodule Pleroma.Object.Fetcher do
 | 
			
		|||
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  @httpoison Application.get_env(:pleroma, :httpoison)
 | 
			
		||||
 | 
			
		||||
  defp reinject_object(data) do
 | 
			
		||||
    Logger.debug("Reinjecting object #{data["id"]}")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -78,7 +77,7 @@ def fetch_and_contain_remote_object_from_id(id) do
 | 
			
		|||
 | 
			
		||||
    with true <- String.starts_with?(id, "http"),
 | 
			
		||||
         {:ok, %{body: body, status: code}} when code in 200..299 <-
 | 
			
		||||
           @httpoison.get(
 | 
			
		||||
           HTTP.get(
 | 
			
		||||
             id,
 | 
			
		||||
             [{:Accept, "application/activity+json"}]
 | 
			
		||||
           ),
 | 
			
		||||
| 
						 | 
				
			
			@ -86,6 +85,9 @@ def fetch_and_contain_remote_object_from_id(id) do
 | 
			
		|||
         :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"}
 | 
			
		||||
 | 
			
		||||
      e ->
 | 
			
		||||
        {:error, e}
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ def init(options) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def call(conn, _opts) do
 | 
			
		||||
    if Keyword.get(Application.get_env(:pleroma, :instance), :federating) do
 | 
			
		||||
    if Pleroma.Config.get([:instance, :federating]) do
 | 
			
		||||
      conn
 | 
			
		||||
    else
 | 
			
		||||
      conn
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,14 +56,14 @@ defp csp_string do
 | 
			
		|||
    connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
 | 
			
		||||
 | 
			
		||||
    connect_src =
 | 
			
		||||
      if Mix.env() == :dev do
 | 
			
		||||
      if Pleroma.Config.get(:env) == :dev do
 | 
			
		||||
        connect_src <> " http://localhost:3035/"
 | 
			
		||||
      else
 | 
			
		||||
        connect_src
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    script_src =
 | 
			
		||||
      if Mix.env() == :dev do
 | 
			
		||||
      if Pleroma.Config.get(:env) == :dev do
 | 
			
		||||
        "script-src 'self' 'unsafe-eval'"
 | 
			
		||||
      else
 | 
			
		||||
        "script-src 'self'"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,36 +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.Plugs.RateLimitPlug do
 | 
			
		||||
  import Phoenix.Controller, only: [json: 2]
 | 
			
		||||
  import Plug.Conn
 | 
			
		||||
 | 
			
		||||
  def init(opts), do: opts
 | 
			
		||||
 | 
			
		||||
  def call(conn, opts) do
 | 
			
		||||
    enabled? = Pleroma.Config.get([:app_account_creation, :enabled])
 | 
			
		||||
 | 
			
		||||
    case check_rate(conn, Map.put(opts, :enabled, enabled?)) do
 | 
			
		||||
      {:ok, _count} -> conn
 | 
			
		||||
      {:error, _count} -> render_error(conn)
 | 
			
		||||
      %Plug.Conn{} = conn -> conn
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp check_rate(conn, %{enabled: true} = opts) do
 | 
			
		||||
    max_requests = opts[:max_requests]
 | 
			
		||||
    bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".")
 | 
			
		||||
 | 
			
		||||
    ExRated.check_rate(bucket_name, opts[:interval] * 1000, max_requests)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp check_rate(conn, _), do: conn
 | 
			
		||||
 | 
			
		||||
  defp render_error(conn) do
 | 
			
		||||
    conn
 | 
			
		||||
    |> put_status(:forbidden)
 | 
			
		||||
    |> json(%{error: "Rate limit exceeded."})
 | 
			
		||||
    |> halt()
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										94
									
								
								lib/pleroma/plugs/rate_limiter.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								lib/pleroma/plugs/rate_limiter.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +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.Plugs.RateLimiter do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
 | 
			
		||||
  ## Configuration
 | 
			
		||||
 | 
			
		||||
  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.
 | 
			
		||||
 | 
			
		||||
  To disable a limiter set its value to `nil`.
 | 
			
		||||
 | 
			
		||||
  ### Example
 | 
			
		||||
 | 
			
		||||
      config :pleroma, :rate_limit,
 | 
			
		||||
        one: {1000, 10},
 | 
			
		||||
        two: [{10_000, 10}, {10_000, 50}],
 | 
			
		||||
        foobar: nil
 | 
			
		||||
 | 
			
		||||
  Here we have three limiters:
 | 
			
		||||
 | 
			
		||||
  * `one` which is not over 10req/1s
 | 
			
		||||
  * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
 | 
			
		||||
  * `foobar` which is disabled
 | 
			
		||||
 | 
			
		||||
  ## Usage
 | 
			
		||||
 | 
			
		||||
  Inside a controller:
 | 
			
		||||
 | 
			
		||||
      plug(Pleroma.Plugs.RateLimiter, :one when action == :one)
 | 
			
		||||
      plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three])
 | 
			
		||||
 | 
			
		||||
  or inside a router pipiline:
 | 
			
		||||
 | 
			
		||||
      pipeline :api do
 | 
			
		||||
        ...
 | 
			
		||||
        plug(Pleroma.Plugs.RateLimiter, :one)
 | 
			
		||||
        ...
 | 
			
		||||
      end
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  import Phoenix.Controller, only: [json: 2]
 | 
			
		||||
  import Plug.Conn
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
 | 
			
		||||
  def init(limiter_name) do
 | 
			
		||||
    case Pleroma.Config.get([:rate_limit, limiter_name]) do
 | 
			
		||||
      nil -> nil
 | 
			
		||||
      config -> {limiter_name, config}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # do not limit if there is no limiter configuration
 | 
			
		||||
  def call(conn, nil), do: conn
 | 
			
		||||
 | 
			
		||||
  def call(conn, opts) do
 | 
			
		||||
    case check_rate(conn, opts) do
 | 
			
		||||
      {:ok, _count} -> conn
 | 
			
		||||
      {:error, _count} -> render_error(conn)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp check_rate(%{assigns: %{user: %User{id: user_id}}}, {limiter_name, [_, {scale, limit}]}) do
 | 
			
		||||
    ExRated.check_rate("#{limiter_name}:#{user_id}", scale, limit)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp check_rate(conn, {limiter_name, [{scale, limit} | _]}) do
 | 
			
		||||
    ExRated.check_rate("#{limiter_name}:#{ip(conn)}", scale, limit)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp check_rate(conn, {limiter_name, {scale, limit}}) do
 | 
			
		||||
    check_rate(conn, {limiter_name, [{scale, limit}]})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def ip(%{remote_ip: remote_ip}) do
 | 
			
		||||
    remote_ip
 | 
			
		||||
    |> Tuple.to_list()
 | 
			
		||||
    |> Enum.join(".")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp render_error(conn) do
 | 
			
		||||
    conn
 | 
			
		||||
    |> put_status(:too_many_requests)
 | 
			
		||||
    |> json(%{error: "Throttled"})
 | 
			
		||||
    |> halt()
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +36,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
 | 
			
		|||
          conn
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    config = Pleroma.Config.get([Pleroma.Upload])
 | 
			
		||||
    config = Pleroma.Config.get(Pleroma.Upload)
 | 
			
		||||
 | 
			
		||||
    with uploader <- Keyword.fetch!(config, :uploader),
 | 
			
		||||
         proxy_remote = Keyword.get(config, :proxy_remote, false),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										66
									
								
								lib/pleroma/release_tasks.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								lib/pleroma/release_tasks.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,66 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.ReleaseTasks do
 | 
			
		||||
  @repo Pleroma.Repo
 | 
			
		||||
 | 
			
		||||
  def run(args) do
 | 
			
		||||
    [task | args] = String.split(args)
 | 
			
		||||
 | 
			
		||||
    case task do
 | 
			
		||||
      "migrate" -> migrate(args)
 | 
			
		||||
      "create" -> create()
 | 
			
		||||
      "rollback" -> rollback(args)
 | 
			
		||||
      task -> mix_task(task, args)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp mix_task(task, args) do
 | 
			
		||||
    Application.load(:pleroma)
 | 
			
		||||
    {:ok, modules} = :application.get_key(:pleroma, :modules)
 | 
			
		||||
 | 
			
		||||
    module =
 | 
			
		||||
      Enum.find(modules, fn module ->
 | 
			
		||||
        module = Module.split(module)
 | 
			
		||||
 | 
			
		||||
        match?(["Mix", "Tasks", "Pleroma" | _], module) and
 | 
			
		||||
          String.downcase(List.last(module)) == task
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    if module do
 | 
			
		||||
      module.run(args)
 | 
			
		||||
    else
 | 
			
		||||
      IO.puts("The task #{task} does not exist")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def migrate(args) do
 | 
			
		||||
    Mix.Tasks.Pleroma.Ecto.Migrate.run(args)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def rollback(args) do
 | 
			
		||||
    Mix.Tasks.Pleroma.Ecto.Rollback.run(args)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create do
 | 
			
		||||
    Application.load(:pleroma)
 | 
			
		||||
 | 
			
		||||
    case @repo.__adapter__.storage_up(@repo.config) do
 | 
			
		||||
      :ok ->
 | 
			
		||||
        IO.puts("The database for #{inspect(@repo)} has been created")
 | 
			
		||||
 | 
			
		||||
      {:error, :already_up} ->
 | 
			
		||||
        IO.puts("The database for #{inspect(@repo)} has already been created")
 | 
			
		||||
 | 
			
		||||
      {:error, term} when is_binary(term) ->
 | 
			
		||||
        IO.puts(:stderr, "The database for #{inspect(@repo)} couldn't be created: #{term}")
 | 
			
		||||
 | 
			
		||||
      {:error, term} ->
 | 
			
		||||
        IO.puts(
 | 
			
		||||
          :stderr,
 | 
			
		||||
          "The database for #{inspect(@repo)} couldn't be created: #{inspect(term)}"
 | 
			
		||||
        )
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -3,6 +3,8 @@
 | 
			
		|||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.ReverseProxy do
 | 
			
		||||
  alias Pleroma.HTTP
 | 
			
		||||
 | 
			
		||||
  @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
 | 
			
		||||
                      ~w(if-unmodified-since if-none-match if-range range)
 | 
			
		||||
  @resp_cache_headers ~w(etag date last-modified cache-control)
 | 
			
		||||
| 
						 | 
				
			
			@ -59,9 +61,6 @@ defmodule Pleroma.ReverseProxy do
 | 
			
		|||
  * `http`: options for [hackney](https://github.com/benoitc/hackney).
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @hackney Application.get_env(:pleroma, :hackney, :hackney)
 | 
			
		||||
  @httpoison Application.get_env(:pleroma, :httpoison, HTTPoison)
 | 
			
		||||
 | 
			
		||||
  @default_hackney_options []
 | 
			
		||||
 | 
			
		||||
  @inline_content_types [
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +96,7 @@ def call(conn = %{method: method}, url, opts) when method in @methods do
 | 
			
		|||
    hackney_opts =
 | 
			
		||||
      @default_hackney_options
 | 
			
		||||
      |> Keyword.merge(Keyword.get(opts, :http, []))
 | 
			
		||||
      |> @httpoison.process_request_options()
 | 
			
		||||
      |> HTTP.process_request_options()
 | 
			
		||||
 | 
			
		||||
    req_headers = build_req_headers(conn.req_headers, opts)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -147,7 +146,7 @@ defp request(method, url, headers, hackney_opts) do
 | 
			
		|||
    Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
 | 
			
		||||
    method = method |> String.downcase() |> String.to_existing_atom()
 | 
			
		||||
 | 
			
		||||
    case @hackney.request(method, url, headers, "", hackney_opts) do
 | 
			
		||||
    case hackney().request(method, url, headers, "", hackney_opts) do
 | 
			
		||||
      {:ok, code, headers, client} when code in @valid_resp_codes ->
 | 
			
		||||
        {:ok, code, downcase_headers(headers), client}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -197,7 +196,7 @@ defp chunk_reply(conn, client, opts, sent_so_far, duration) do
 | 
			
		|||
             duration,
 | 
			
		||||
             Keyword.get(opts, :max_read_duration, @max_read_duration)
 | 
			
		||||
           ),
 | 
			
		||||
         {:ok, data} <- @hackney.stream_body(client),
 | 
			
		||||
         {:ok, data} <- hackney().stream_body(client),
 | 
			
		||||
         {:ok, duration} <- increase_read_duration(duration),
 | 
			
		||||
         sent_so_far = sent_so_far + byte_size(data),
 | 
			
		||||
         :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)),
 | 
			
		||||
| 
						 | 
				
			
			@ -378,4 +377,6 @@ defp increase_read_duration({previous_duration, started})
 | 
			
		|||
  defp increase_read_duration(_) do
 | 
			
		||||
    {:ok, :no_duration_limit, :no_duration_limit}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp hackney, do: Pleroma.Config.get(:hackney, :hackney)
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,11 +4,10 @@
 | 
			
		|||
 | 
			
		||||
defmodule Pleroma.Uploaders.MDII do
 | 
			
		||||
  alias Pleroma.Config
 | 
			
		||||
  alias Pleroma.HTTP
 | 
			
		||||
 | 
			
		||||
  @behaviour Pleroma.Uploaders.Uploader
 | 
			
		||||
 | 
			
		||||
  @httpoison Application.get_env(:pleroma, :httpoison)
 | 
			
		||||
 | 
			
		||||
  # MDII-hosted images are never passed through the MediaPlug; only local media.
 | 
			
		||||
  # Delegate to Pleroma.Uploaders.Local
 | 
			
		||||
  def get_file(file) do
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +24,7 @@ def put_file(upload) do
 | 
			
		|||
    query = "#{cgi}?#{extension}"
 | 
			
		||||
 | 
			
		||||
    with {:ok, %{status: 200, body: body}} <-
 | 
			
		||||
           @httpoison.post(query, file_data, [], adapter: [pool: :default]) do
 | 
			
		||||
           HTTP.post(query, file_data, [], adapter: [pool: :default]) do
 | 
			
		||||
      remote_file_name = String.split(body) |> List.first()
 | 
			
		||||
      public_url = "#{files}/#{remote_file_name}.#{extension}"
 | 
			
		||||
      {:ok, {:url, public_url}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -324,14 +324,6 @@ def maybe_direct_follow(%User{} = follower, %User{} = followed) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
 | 
			
		||||
    if not following?(follower, 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
 | 
			
		||||
| 
						 | 
				
			
			@ -366,14 +358,12 @@ def follow_all(follower, followeds) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def follow(%User{} = follower, %User{info: info} = followed) do
 | 
			
		||||
    user_config = Application.get_env(:pleroma, :user)
 | 
			
		||||
    deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
 | 
			
		||||
 | 
			
		||||
    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
 | 
			
		||||
    ap_followers = followed.follower_address
 | 
			
		||||
 | 
			
		||||
    cond do
 | 
			
		||||
      following?(follower, followed) or info.deactivated ->
 | 
			
		||||
        {:error, "Could not follow user: #{followed.nickname} is already on your list."}
 | 
			
		||||
      info.deactivated ->
 | 
			
		||||
        {:error, "Could not follow user: You are deactivated."}
 | 
			
		||||
 | 
			
		||||
      deny_follow_blocked and blocks?(followed, follower) ->
 | 
			
		||||
        {:error, "Could not follow user: #{followed.nickname} blocked you."}
 | 
			
		||||
| 
						 | 
				
			
			@ -737,122 +727,6 @@ def get_recipients_from_activity(%Activity{recipients: to}) do
 | 
			
		|||
    |> Repo.all()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search(query, resolve \\ false, for_user \\ nil) do
 | 
			
		||||
    # Strip the beginning @ off if there is a query
 | 
			
		||||
    query = String.trim_leading(query, "@")
 | 
			
		||||
 | 
			
		||||
    if resolve, do: get_or_fetch(query)
 | 
			
		||||
 | 
			
		||||
    {:ok, results} =
 | 
			
		||||
      Repo.transaction(fn ->
 | 
			
		||||
        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
 | 
			
		||||
        Repo.all(search_query(query, for_user))
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    results
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search_query(query, for_user) do
 | 
			
		||||
    fts_subquery = fts_search_subquery(query)
 | 
			
		||||
    trigram_subquery = trigram_search_subquery(query)
 | 
			
		||||
    union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
 | 
			
		||||
    distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
 | 
			
		||||
 | 
			
		||||
    from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
 | 
			
		||||
      order_by: [desc: s.search_rank],
 | 
			
		||||
      limit: 20
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp boost_search_rank_query(query, nil), do: query
 | 
			
		||||
 | 
			
		||||
  defp boost_search_rank_query(query, for_user) do
 | 
			
		||||
    friends_ids = get_friends_ids(for_user)
 | 
			
		||||
    followers_ids = get_followers_ids(for_user)
 | 
			
		||||
 | 
			
		||||
    from(u in subquery(query),
 | 
			
		||||
      select_merge: %{
 | 
			
		||||
        search_rank:
 | 
			
		||||
          fragment(
 | 
			
		||||
            """
 | 
			
		||||
             CASE WHEN (?) THEN (?) * 1.3
 | 
			
		||||
             WHEN (?) THEN (?) * 1.2
 | 
			
		||||
             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
 | 
			
		||||
 | 
			
		||||
  defp fts_search_subquery(term, query \\ User) do
 | 
			
		||||
    processed_query =
 | 
			
		||||
      term
 | 
			
		||||
      |> String.replace(~r/\W+/, " ")
 | 
			
		||||
      |> 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
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    |> restrict_deactivated()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp trigram_search_subquery(term) do
 | 
			
		||||
    from(
 | 
			
		||||
      u in User,
 | 
			
		||||
      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)
 | 
			
		||||
    )
 | 
			
		||||
    |> restrict_deactivated()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def mute(muter, %User{ap_id: ap_id}) do
 | 
			
		||||
    info_cng =
 | 
			
		||||
      muter.info
 | 
			
		||||
| 
						 | 
				
			
			@ -1162,9 +1036,7 @@ def html_filter_policy(%User{info: %{no_rich_text: true}}) do
 | 
			
		|||
    Pleroma.HTML.Scrubber.TwitterText
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
 | 
			
		||||
 | 
			
		||||
  def html_filter_policy(_), do: @default_scrubbers
 | 
			
		||||
  def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
 | 
			
		||||
 | 
			
		||||
  def fetch_by_ap_id(ap_id) do
 | 
			
		||||
    ap_try = ActivityPub.make_user_from_ap_id(ap_id)
 | 
			
		||||
| 
						 | 
				
			
			@ -1443,4 +1315,14 @@ def ensure_keys_present(user) do
 | 
			
		|||
      update_and_set_cache(cng)
 | 
			
		||||
    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
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,14 +42,21 @@ defmodule Pleroma.User.Info do
 | 
			
		|||
    field(:hide_follows, :boolean, default: false)
 | 
			
		||||
    field(:hide_favorites, :boolean, default: true)
 | 
			
		||||
    field(:pinned_activities, {:array, :string}, default: [])
 | 
			
		||||
    field(:flavour, :string, default: nil)
 | 
			
		||||
    field(:mascot, :map, default: nil)
 | 
			
		||||
    field(:emoji, {:array, :map}, default: [])
 | 
			
		||||
    field(:pleroma_settings_store, :map, default: %{})
 | 
			
		||||
 | 
			
		||||
    field(:notification_settings, :map,
 | 
			
		||||
      default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true}
 | 
			
		||||
      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?
 | 
			
		||||
| 
						 | 
				
			
			@ -68,10 +75,15 @@ def set_activation_status(info, deactivated) do
 | 
			
		|||
  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(["remote", "local", "followers", "follows"])
 | 
			
		||||
      |> Map.take(["followers", "follows", "non_follows", "non_followers"])
 | 
			
		||||
 | 
			
		||||
    params = %{notification_settings: notification_settings}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -209,7 +221,9 @@ def profile_update(info, params) do
 | 
			
		|||
      :hide_followers,
 | 
			
		||||
      :hide_favorites,
 | 
			
		||||
      :background,
 | 
			
		||||
      :show_role
 | 
			
		||||
      :show_role,
 | 
			
		||||
      :skip_thread_containment,
 | 
			
		||||
      :pleroma_settings_store
 | 
			
		||||
    ])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -241,14 +255,6 @@ def mastodon_settings_update(info, settings) do
 | 
			
		|||
    |> validate_required([:settings])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def mastodon_flavour_update(info, flavour) do
 | 
			
		||||
    params = %{flavour: flavour}
 | 
			
		||||
 | 
			
		||||
    info
 | 
			
		||||
    |> cast(params, [:flavour])
 | 
			
		||||
    |> validate_required([:flavour])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def mascot_update(info, url) do
 | 
			
		||||
    params = %{mascot: url}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										191
									
								
								lib/pleroma/user/search.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								lib/pleroma/user/search.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +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.User.Search do
 | 
			
		||||
  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)
 | 
			
		||||
 | 
			
		||||
    # Strip the beginning @ off if there is a query
 | 
			
		||||
    query_string = String.trim_leading(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)
 | 
			
		||||
        |> paginate(result_limit, offset)
 | 
			
		||||
        |> Repo.all()
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    results
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp search_query(query_string, for_user, following) do
 | 
			
		||||
    for_user
 | 
			
		||||
    |> base_query(following)
 | 
			
		||||
    |> search_subqueries(query_string)
 | 
			
		||||
    |> union_subqueries
 | 
			
		||||
    |> distinct_query()
 | 
			
		||||
    |> boost_search_rank_query(for_user)
 | 
			
		||||
    |> subquery()
 | 
			
		||||
    |> order_by(desc: :search_rank)
 | 
			
		||||
    |> maybe_restrict_local(for_user)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp base_query(_user, false), do: User
 | 
			
		||||
  defp base_query(user, true), do: User.get_followers_query(user)
 | 
			
		||||
 | 
			
		||||
  defp paginate(query, limit, offset) do
 | 
			
		||||
    from(q in query, limit: ^limit, offset: ^offset)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  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 boost_search_rank_query(query, 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
 | 
			
		||||
             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 =
 | 
			
		||||
      term
 | 
			
		||||
      |> String.replace(~r/\W+/, " ")
 | 
			
		||||
      |> 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
 | 
			
		||||
    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
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
 | 
			
		||||
defmodule Pleroma.Web.ActivityPub.ActivityPub do
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.Config
 | 
			
		||||
  alias Pleroma.Conversation
 | 
			
		||||
  alias Pleroma.Notification
 | 
			
		||||
  alias Pleroma.Object
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +74,7 @@ defp check_actor_is_active(actor) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
 | 
			
		||||
    limit = Pleroma.Config.get([:instance, :remote_limit])
 | 
			
		||||
    limit = Config.get([:instance, :remote_limit])
 | 
			
		||||
    String.length(content) <= limit
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -108,6 +109,15 @@ def decrease_replies_count_if_reply(%Object{
 | 
			
		|||
 | 
			
		||||
  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) when is_map(map) do
 | 
			
		||||
    with nil <- Activity.normalize(map),
 | 
			
		||||
         map <- lazy_put_activity_defaults(map, fake),
 | 
			
		||||
| 
						 | 
				
			
			@ -183,40 +193,42 @@ def stream_out(activity) do
 | 
			
		|||
    public = "https://www.w3.org/ns/activitystreams#Public"
 | 
			
		||||
 | 
			
		||||
    if activity.data["type"] in ["Create", "Announce", "Delete"] do
 | 
			
		||||
      Pleroma.Web.Streamer.stream("user", activity)
 | 
			
		||||
      Pleroma.Web.Streamer.stream("list", activity)
 | 
			
		||||
      object = Object.normalize(activity)
 | 
			
		||||
      # Do not stream out poll replies
 | 
			
		||||
      unless object.data["type"] == "Answer" do
 | 
			
		||||
        Pleroma.Web.Streamer.stream("user", activity)
 | 
			
		||||
        Pleroma.Web.Streamer.stream("list", activity)
 | 
			
		||||
 | 
			
		||||
      if Enum.member?(activity.data["to"], public) do
 | 
			
		||||
        Pleroma.Web.Streamer.stream("public", activity)
 | 
			
		||||
        if Enum.member?(activity.data["to"], public) do
 | 
			
		||||
          Pleroma.Web.Streamer.stream("public", activity)
 | 
			
		||||
 | 
			
		||||
        if activity.local do
 | 
			
		||||
          Pleroma.Web.Streamer.stream("public:local", activity)
 | 
			
		||||
        end
 | 
			
		||||
          if activity.local do
 | 
			
		||||
            Pleroma.Web.Streamer.stream("public:local", activity)
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
        if activity.data["type"] in ["Create"] do
 | 
			
		||||
          object = Object.normalize(activity)
 | 
			
		||||
          if activity.data["type"] in ["Create"] do
 | 
			
		||||
            object.data
 | 
			
		||||
            |> Map.get("tag", [])
 | 
			
		||||
            |> Enum.filter(fn tag -> is_bitstring(tag) end)
 | 
			
		||||
            |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
 | 
			
		||||
 | 
			
		||||
          object.data
 | 
			
		||||
          |> Map.get("tag", [])
 | 
			
		||||
          |> Enum.filter(fn tag -> is_bitstring(tag) end)
 | 
			
		||||
          |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
 | 
			
		||||
            if object.data["attachment"] != [] do
 | 
			
		||||
              Pleroma.Web.Streamer.stream("public:media", activity)
 | 
			
		||||
 | 
			
		||||
          if object.data["attachment"] != [] do
 | 
			
		||||
            Pleroma.Web.Streamer.stream("public:media", activity)
 | 
			
		||||
 | 
			
		||||
            if activity.local do
 | 
			
		||||
              Pleroma.Web.Streamer.stream("public:local:media", activity)
 | 
			
		||||
              if activity.local do
 | 
			
		||||
                Pleroma.Web.Streamer.stream("public:local:media", activity)
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
        else
 | 
			
		||||
          # TODO: Write test, replace with visibility test
 | 
			
		||||
          if !Enum.member?(activity.data["cc"] || [], public) &&
 | 
			
		||||
               !Enum.member?(
 | 
			
		||||
                 activity.data["to"],
 | 
			
		||||
                 User.get_cached_by_ap_id(activity.data["actor"]).follower_address
 | 
			
		||||
               ),
 | 
			
		||||
             do: Pleroma.Web.Streamer.stream("direct", activity)
 | 
			
		||||
        end
 | 
			
		||||
      else
 | 
			
		||||
        # TODO: Write test, replace with visibility test
 | 
			
		||||
        if !Enum.member?(activity.data["cc"] || [], public) &&
 | 
			
		||||
             !Enum.member?(
 | 
			
		||||
               activity.data["to"],
 | 
			
		||||
               User.get_cached_by_ap_id(activity.data["actor"]).follower_address
 | 
			
		||||
             ),
 | 
			
		||||
           do: Pleroma.Web.Streamer.stream("direct", activity)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -235,6 +247,7 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f
 | 
			
		|||
         {: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
 | 
			
		||||
         {:ok, _actor} <- increase_note_count_if_public(actor, activity),
 | 
			
		||||
| 
						 | 
				
			
			@ -399,16 +412,12 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def block(blocker, blocked, activity_id \\ nil, local \\ true) do
 | 
			
		||||
    ap_config = Application.get_env(:pleroma, :activitypub)
 | 
			
		||||
    unfollow_blocked = Keyword.get(ap_config, :unfollow_blocked)
 | 
			
		||||
    outgoing_blocks = Keyword.get(ap_config, :outgoing_blocks)
 | 
			
		||||
    outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
 | 
			
		||||
    unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
 | 
			
		||||
 | 
			
		||||
    with true <- unfollow_blocked do
 | 
			
		||||
    if unfollow_blocked do
 | 
			
		||||
      follow_activity = fetch_latest_follow(blocker, blocked)
 | 
			
		||||
 | 
			
		||||
      if follow_activity do
 | 
			
		||||
        unfollow(blocker, blocked, nil, local)
 | 
			
		||||
      end
 | 
			
		||||
      if follow_activity, do: unfollow(blocker, blocked, nil, local)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    with true <- outgoing_blocks,
 | 
			
		||||
| 
						 | 
				
			
			@ -480,6 +489,7 @@ defp fetch_activities_for_context_query(context, opts) do
 | 
			
		|||
      if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
 | 
			
		||||
 | 
			
		||||
    from(activity in Activity)
 | 
			
		||||
    |> maybe_preload_objects(opts)
 | 
			
		||||
    |> restrict_blocked(opts)
 | 
			
		||||
    |> restrict_recipients(recipients, opts["user"])
 | 
			
		||||
    |> where(
 | 
			
		||||
| 
						 | 
				
			
			@ -492,6 +502,7 @@ defp fetch_activities_for_context_query(context, opts) do
 | 
			
		|||
        ^context
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
    |> exclude_poll_votes(opts)
 | 
			
		||||
    |> order_by([activity], desc: activity.id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -499,7 +510,6 @@ defp fetch_activities_for_context_query(context, opts) do
 | 
			
		|||
  def fetch_activities_for_context(context, opts \\ %{}) do
 | 
			
		||||
    context
 | 
			
		||||
    |> fetch_activities_for_context_query(opts)
 | 
			
		||||
    |> Activity.with_preloaded_object()
 | 
			
		||||
    |> Repo.all()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -507,7 +517,7 @@ def fetch_activities_for_context(context, opts \\ %{}) do
 | 
			
		|||
          Pleroma.FlakeId.t() | nil
 | 
			
		||||
  def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
 | 
			
		||||
    context
 | 
			
		||||
    |> fetch_activities_for_context_query(opts)
 | 
			
		||||
    |> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
 | 
			
		||||
    |> limit(1)
 | 
			
		||||
    |> select([a], a.id)
 | 
			
		||||
    |> Repo.one()
 | 
			
		||||
| 
						 | 
				
			
			@ -548,14 +558,11 @@ defp restrict_visibility(query, %{visibility: visibility})
 | 
			
		|||
 | 
			
		||||
  defp restrict_visibility(query, %{visibility: visibility})
 | 
			
		||||
       when visibility in @valid_visibilities do
 | 
			
		||||
    query =
 | 
			
		||||
      from(
 | 
			
		||||
        a in query,
 | 
			
		||||
        where:
 | 
			
		||||
          fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    query
 | 
			
		||||
    from(
 | 
			
		||||
      a in query,
 | 
			
		||||
      where:
 | 
			
		||||
        fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp restrict_visibility(_query, %{visibility: visibility})
 | 
			
		||||
| 
						 | 
				
			
			@ -565,17 +572,24 @@ defp restrict_visibility(_query, %{visibility: visibility})
 | 
			
		|||
 | 
			
		||||
  defp restrict_visibility(query, _visibility), do: query
 | 
			
		||||
 | 
			
		||||
  defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}) do
 | 
			
		||||
    query =
 | 
			
		||||
      from(
 | 
			
		||||
        a in query,
 | 
			
		||||
        where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
 | 
			
		||||
      )
 | 
			
		||||
  defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
 | 
			
		||||
    do: query
 | 
			
		||||
 | 
			
		||||
    query
 | 
			
		||||
  defp restrict_thread_visibility(
 | 
			
		||||
         query,
 | 
			
		||||
         %{"user" => %User{info: %{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
 | 
			
		||||
  defp restrict_thread_visibility(query, _, _), do: query
 | 
			
		||||
 | 
			
		||||
  def fetch_user_activities(user, reading_user, params \\ %{}) do
 | 
			
		||||
    params =
 | 
			
		||||
| 
						 | 
				
			
			@ -653,20 +667,6 @@ defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
 | 
			
		|||
 | 
			
		||||
  defp restrict_tag(query, _), do: query
 | 
			
		||||
 | 
			
		||||
  defp restrict_to_cc(query, recipients_to, recipients_cc) do
 | 
			
		||||
    from(
 | 
			
		||||
      activity in query,
 | 
			
		||||
      where:
 | 
			
		||||
        fragment(
 | 
			
		||||
          "(?->'to' \\?| ?) or (?->'cc' \\?| ?)",
 | 
			
		||||
          activity.data,
 | 
			
		||||
          ^recipients_to,
 | 
			
		||||
          activity.data,
 | 
			
		||||
          ^recipients_cc
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp restrict_recipients(query, [], _user), do: query
 | 
			
		||||
 | 
			
		||||
  defp restrict_recipients(query, recipients, nil) do
 | 
			
		||||
| 
						 | 
				
			
			@ -820,6 +820,18 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
 | 
			
		|||
 | 
			
		||||
  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 maybe_preload_objects(query, %{"skip_preload" => true}), do: query
 | 
			
		||||
 | 
			
		||||
  defp maybe_preload_objects(query, _) do
 | 
			
		||||
| 
						 | 
				
			
			@ -856,6 +868,10 @@ defp maybe_order(query, _), do: query
 | 
			
		|||
  def fetch_activities_query(recipients, opts \\ %{}) do
 | 
			
		||||
    base_query = from(activity in Activity)
 | 
			
		||||
 | 
			
		||||
    config = %{
 | 
			
		||||
      skip_thread_containment: Config.get([:instance, :skip_thread_containment])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    base_query
 | 
			
		||||
    |> maybe_preload_objects(opts)
 | 
			
		||||
    |> maybe_preload_bookmarks(opts)
 | 
			
		||||
| 
						 | 
				
			
			@ -875,12 +891,13 @@ def fetch_activities_query(recipients, opts \\ %{}) do
 | 
			
		|||
    |> restrict_muted(opts)
 | 
			
		||||
    |> restrict_media(opts)
 | 
			
		||||
    |> restrict_visibility(opts)
 | 
			
		||||
    |> restrict_thread_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)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def fetch_activities(recipients, opts \\ %{}) do
 | 
			
		||||
| 
						 | 
				
			
			@ -889,9 +906,18 @@ def fetch_activities(recipients, opts \\ %{}) do
 | 
			
		|||
    |> Enum.reverse()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
 | 
			
		||||
  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
 | 
			
		||||
             "https://www.w3.org/ns/activitystreams#Public" in activity.recipients)
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def fetch_activities_bounded(recipients, recipients_with_public, opts \\ %{}) do
 | 
			
		||||
    fetch_activities_query([], opts)
 | 
			
		||||
    |> restrict_to_cc(recipients_to, recipients_cc)
 | 
			
		||||
    |> fetch_activities_bounded_query(recipients, recipients_with_public)
 | 
			
		||||
    |> Pagination.fetch_paginated(opts)
 | 
			
		||||
    |> Enum.reverse()
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
 | 
			
		|||
  plug(:relay_active? when action in [:relay])
 | 
			
		||||
 | 
			
		||||
  def relay_active?(conn, _) do
 | 
			
		||||
    if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do
 | 
			
		||||
    if Pleroma.Config.get([:instance, :allow_relay]) do
 | 
			
		||||
      conn
 | 
			
		||||
    else
 | 
			
		||||
      conn
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,8 +5,8 @@
 | 
			
		|||
defmodule Pleroma.Web.ActivityPub.MRF do
 | 
			
		||||
  @callback filter(Map.t()) :: {:ok | :reject, Map.t()}
 | 
			
		||||
 | 
			
		||||
  def filter(object) do
 | 
			
		||||
    get_policies()
 | 
			
		||||
  def filter(policies, %{} = object) do
 | 
			
		||||
    policies
 | 
			
		||||
    |> Enum.reduce({:ok, object}, fn
 | 
			
		||||
      policy, {:ok, object} ->
 | 
			
		||||
        policy.filter(object)
 | 
			
		||||
| 
						 | 
				
			
			@ -16,10 +16,10 @@ def filter(object) do
 | 
			
		|||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def filter(%{} = object), do: get_policies() |> filter(object)
 | 
			
		||||
 | 
			
		||||
  def get_policies do
 | 
			
		||||
    Application.get_env(:pleroma, :instance, [])
 | 
			
		||||
    |> Keyword.get(:rewrite_policy, [])
 | 
			
		||||
    |> get_policies()
 | 
			
		||||
    Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_policies(policy) when is_atom(policy), do: [policy]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										48
									
								
								lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
# 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
 | 
			
		||||
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  # has the user successfully posted before?
 | 
			
		||||
  defp old_user?(%User{} = u) do
 | 
			
		||||
    u.info.note_count > 0 || u.info.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
 | 
			
		||||
 | 
			
		||||
  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}
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -74,8 +74,7 @@ defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
 | 
			
		|||
               actor_host
 | 
			
		||||
             ),
 | 
			
		||||
           user <- User.get_cached_by_ap_id(object["actor"]),
 | 
			
		||||
           true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"],
 | 
			
		||||
           true <- user.follower_address in object["cc"] do
 | 
			
		||||
           true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"] do
 | 
			
		||||
        to =
 | 
			
		||||
          List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++
 | 
			
		||||
            [user.follower_address]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										40
									
								
								lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								lib/pleroma/web/activity_pub/mrf/subchain_policy.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
# 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.MRF.SubchainPolicy do
 | 
			
		||||
  alias Pleroma.Config
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.MRF
 | 
			
		||||
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  @behaviour MRF
 | 
			
		||||
 | 
			
		||||
  defp lookup_subchain(actor) do
 | 
			
		||||
    with matches <- Config.get([:mrf_subchain, :match_actor]),
 | 
			
		||||
         {match, subchain} <- Enum.find(matches, fn {k, _v} -> String.match?(actor, k) end) do
 | 
			
		||||
      {:ok, match, subchain}
 | 
			
		||||
    else
 | 
			
		||||
      _e -> {:error, :notfound}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def filter(%{"actor" => actor} = message) do
 | 
			
		||||
    with {:ok, match, subchain} <- lookup_subchain(actor) do
 | 
			
		||||
      Logger.debug(
 | 
			
		||||
        "[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{
 | 
			
		||||
          inspect(subchain)
 | 
			
		||||
        }"
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      subchain
 | 
			
		||||
      |> MRF.filter(message)
 | 
			
		||||
    else
 | 
			
		||||
      _e -> {:ok, message}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def filter(message), do: {:ok, message}
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -5,6 +5,7 @@
 | 
			
		|||
defmodule Pleroma.Web.ActivityPub.Publisher do
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.Config
 | 
			
		||||
  alias Pleroma.HTTP
 | 
			
		||||
  alias Pleroma.Instances
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.Relay
 | 
			
		||||
| 
						 | 
				
			
			@ -16,8 +17,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
 | 
			
		|||
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  @httpoison Application.get_env(:pleroma, :httpoison)
 | 
			
		||||
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  ActivityPub outgoing federation module.
 | 
			
		||||
  """
 | 
			
		||||
| 
						 | 
				
			
			@ -63,7 +62,7 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
 | 
			
		|||
 | 
			
		||||
    with {:ok, %{status: code}} when code in 200..299 <-
 | 
			
		||||
           result =
 | 
			
		||||
             @httpoison.post(
 | 
			
		||||
             HTTP.post(
 | 
			
		||||
               inbox,
 | 
			
		||||
               json,
 | 
			
		||||
               [
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +88,7 @@ defp should_federate?(inbox, public) do
 | 
			
		|||
      true
 | 
			
		||||
    else
 | 
			
		||||
      inbox_info = URI.parse(inbox)
 | 
			
		||||
      !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
 | 
			
		||||
      !Enum.member?(Config.get([:instance, :quarantined_instances], []), inbox_info.host)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,7 @@ def fix_object(object) do
 | 
			
		|||
    |> fix_likes
 | 
			
		||||
    |> fix_addressing
 | 
			
		||||
    |> fix_summary
 | 
			
		||||
    |> fix_type
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def fix_summary(%{"summary" => nil} = object) do
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +66,11 @@ def fix_addressing_list(map, field) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do
 | 
			
		||||
  def fix_explicit_addressing(
 | 
			
		||||
        %{"to" => to, "cc" => cc} = object,
 | 
			
		||||
        explicit_mentions,
 | 
			
		||||
        follower_collection
 | 
			
		||||
      ) do
 | 
			
		||||
    explicit_to =
 | 
			
		||||
      to
 | 
			
		||||
      |> Enum.filter(fn x -> x in explicit_mentions end)
 | 
			
		||||
| 
						 | 
				
			
			@ -76,6 +81,7 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mention
 | 
			
		|||
 | 
			
		||||
    final_cc =
 | 
			
		||||
      (cc ++ explicit_cc)
 | 
			
		||||
      |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
 | 
			
		||||
      |> Enum.uniq()
 | 
			
		||||
 | 
			
		||||
    object
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +89,7 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mention
 | 
			
		|||
    |> Map.put("cc", final_cc)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def fix_explicit_addressing(object, _explicit_mentions), do: object
 | 
			
		||||
  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
 | 
			
		||||
| 
						 | 
				
			
			@ -93,10 +99,12 @@ def fix_explicit_addressing(object) do
 | 
			
		|||
      object
 | 
			
		||||
      |> Utils.determine_explicit_mentions()
 | 
			
		||||
 | 
			
		||||
    explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"]
 | 
			
		||||
    follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
 | 
			
		||||
 | 
			
		||||
    object
 | 
			
		||||
    |> fix_explicit_addressing(explicit_mentions)
 | 
			
		||||
    explicit_mentions =
 | 
			
		||||
      explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#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
 | 
			
		||||
| 
						 | 
				
			
			@ -133,7 +141,7 @@ def fix_addressing(object) do
 | 
			
		|||
    |> fix_addressing_list("cc")
 | 
			
		||||
    |> fix_addressing_list("bto")
 | 
			
		||||
    |> fix_addressing_list("bcc")
 | 
			
		||||
    |> fix_explicit_addressing
 | 
			
		||||
    |> fix_explicit_addressing()
 | 
			
		||||
    |> fix_implicit_addressing(followers_collection)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -328,6 +336,18 @@ def fix_content_map(%{"contentMap" => content_map} = object) do
 | 
			
		|||
 | 
			
		||||
  def fix_content_map(object), do: object
 | 
			
		||||
 | 
			
		||||
  def fix_type(%{"inReplyTo" => reply_id} = object) when is_binary(reply_id) do
 | 
			
		||||
    reply = Object.normalize(reply_id)
 | 
			
		||||
 | 
			
		||||
    if reply && (reply.data["type"] == "Question" and object["name"]) 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),
 | 
			
		||||
| 
						 | 
				
			
			@ -398,7 +418,7 @@ def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8),
 | 
			
		|||
  # - tags
 | 
			
		||||
  # - emoji
 | 
			
		||||
  def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
 | 
			
		||||
      when objtype in ["Article", "Note", "Video", "Page"] do
 | 
			
		||||
      when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
 | 
			
		||||
    actor = Containment.get_actor(data)
 | 
			
		||||
 | 
			
		||||
    data =
 | 
			
		||||
| 
						 | 
				
			
			@ -438,10 +458,12 @@ def handle_incoming(
 | 
			
		|||
         {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
 | 
			
		||||
         {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
 | 
			
		||||
      with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
 | 
			
		||||
           {:user_blocked, false} <-
 | 
			
		||||
           {_, false} <-
 | 
			
		||||
             {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
 | 
			
		||||
           {:user_locked, false} <- {:user_locked, User.locked?(followed)},
 | 
			
		||||
           {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
 | 
			
		||||
           {_, 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,
 | 
			
		||||
| 
						 | 
				
			
			@ -450,7 +472,7 @@ def handle_incoming(
 | 
			
		|||
        })
 | 
			
		||||
      else
 | 
			
		||||
        {:user_blocked, true} ->
 | 
			
		||||
          {:ok, _} = Utils.update_follow_state(activity, "reject")
 | 
			
		||||
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
 | 
			
		||||
 | 
			
		||||
          ActivityPub.reject(%{
 | 
			
		||||
            to: [follower.ap_id],
 | 
			
		||||
| 
						 | 
				
			
			@ -460,7 +482,7 @@ def handle_incoming(
 | 
			
		|||
          })
 | 
			
		||||
 | 
			
		||||
        {:follow, {:error, _}} ->
 | 
			
		||||
          {:ok, _} = Utils.update_follow_state(activity, "reject")
 | 
			
		||||
          {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
 | 
			
		||||
 | 
			
		||||
          ActivityPub.reject(%{
 | 
			
		||||
            to: [follower.ap_id],
 | 
			
		||||
| 
						 | 
				
			
			@ -486,21 +508,16 @@ def handle_incoming(
 | 
			
		|||
    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(follow_activity, "accept"),
 | 
			
		||||
         {: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, activity} <-
 | 
			
		||||
           ActivityPub.accept(%{
 | 
			
		||||
             to: follow_activity.data["to"],
 | 
			
		||||
             type: "Accept",
 | 
			
		||||
             actor: followed,
 | 
			
		||||
             object: follow_activity.data["id"],
 | 
			
		||||
             local: false
 | 
			
		||||
           }) do
 | 
			
		||||
      if not User.following?(follower, followed) do
 | 
			
		||||
        {:ok, _follower} = User.follow(follower, followed)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      {:ok, activity}
 | 
			
		||||
         {: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
 | 
			
		||||
      })
 | 
			
		||||
    else
 | 
			
		||||
      _e -> :error
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -512,7 +529,7 @@ def handle_incoming(
 | 
			
		|||
    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(follow_activity, "reject"),
 | 
			
		||||
         {: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(%{
 | 
			
		||||
| 
						 | 
				
			
			@ -731,6 +748,7 @@ def prepare_object(object) do
 | 
			
		|||
    |> set_reply_to_uri
 | 
			
		||||
    |> strip_internal_fields
 | 
			
		||||
    |> strip_internal_tags
 | 
			
		||||
    |> set_type
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  #  @doc
 | 
			
		||||
| 
						 | 
				
			
			@ -895,6 +913,12 @@ def set_sensitive(object) do
 | 
			
		|||
    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"]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
 | 
			
		|||
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  @supported_object_types ["Article", "Note", "Video", "Page"]
 | 
			
		||||
  @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"]
 | 
			
		||||
  @supported_report_states ~w(open closed resolved)
 | 
			
		||||
  @valid_visibilities ~w(public unlisted private direct)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -376,8 +376,8 @@ def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
 | 
			
		|||
  @doc """
 | 
			
		||||
  Updates a follow activity's state (for locked accounts).
 | 
			
		||||
  """
 | 
			
		||||
  def update_follow_state(
 | 
			
		||||
        %Activity{data: %{"actor" => actor, "object" => object, "state" => "pending"}} = activity,
 | 
			
		||||
  def update_follow_state_for_all(
 | 
			
		||||
        %Activity{data: %{"actor" => actor, "object" => object}} = activity,
 | 
			
		||||
        state
 | 
			
		||||
      ) do
 | 
			
		||||
    try do
 | 
			
		||||
| 
						 | 
				
			
			@ -789,4 +789,22 @@ defp get_updated_targets(
 | 
			
		|||
        [to, cc, recipients]
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_existing_votes(actor, %{data: %{"id" => id}}) do
 | 
			
		||||
    query =
 | 
			
		||||
      from(
 | 
			
		||||
        [activity, object: object] in Activity.with_preloaded_object(Activity),
 | 
			
		||||
        where: fragment("(?)->>'type' = 'Create'", activity.data),
 | 
			
		||||
        where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
 | 
			
		||||
        where:
 | 
			
		||||
          fragment(
 | 
			
		||||
            "(?)->>'inReplyTo' = ?",
 | 
			
		||||
            object.data,
 | 
			
		||||
            ^to_string(id)
 | 
			
		||||
          ),
 | 
			
		||||
        where: fragment("(?)->>'type' = 'Answer'", object.data)
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    Repo.all(query)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,6 +66,9 @@ def get_visibility(object) do
 | 
			
		|||
      Enum.any?(to, &String.contains?(&1, "/followers")) ->
 | 
			
		||||
        "private"
 | 
			
		||||
 | 
			
		||||
      object.data["directMessage"] == true ->
 | 
			
		||||
        "direct"
 | 
			
		||||
 | 
			
		||||
      length(cc) > 0 ->
 | 
			
		||||
        "private"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
 | 
			
		|||
  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.ReportView
 | 
			
		||||
  alias Pleroma.Web.AdminAPI.Search
 | 
			
		||||
  alias Pleroma.Web.CommonAPI
 | 
			
		||||
| 
						 | 
				
			
			@ -362,6 +364,41 @@ def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
 | 
			
		|||
    end
 | 
			
		||||
  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
 | 
			
		||||
            %{"key" => key, "value" => value} ->
 | 
			
		||||
              {:ok, config} = Config.update_or_create(%{key: key, value: value})
 | 
			
		||||
              config
 | 
			
		||||
 | 
			
		||||
            %{"key" => key, "delete" => "true"} ->
 | 
			
		||||
              {:ok, _} = Config.delete(key)
 | 
			
		||||
              nil
 | 
			
		||||
          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)])
 | 
			
		||||
        updated
 | 
			
		||||
      else
 | 
			
		||||
        []
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    conn
 | 
			
		||||
    |> put_view(ConfigView)
 | 
			
		||||
    |> render("index.json", %{configs: updated})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def errors(conn, {:error, :not_found}) do
 | 
			
		||||
    conn
 | 
			
		||||
    |> put_status(404)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										144
									
								
								lib/pleroma/web/admin_api/config.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								lib/pleroma/web/admin_api/config.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,144 @@
 | 
			
		|||
# 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.Config do
 | 
			
		||||
  use Ecto.Schema
 | 
			
		||||
  import Ecto.Changeset
 | 
			
		||||
  alias __MODULE__
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
 | 
			
		||||
  @type t :: %__MODULE__{}
 | 
			
		||||
 | 
			
		||||
  schema "config" do
 | 
			
		||||
    field(:key, :string)
 | 
			
		||||
    field(:value, :binary)
 | 
			
		||||
 | 
			
		||||
    timestamps()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec get_by_key(String.t()) :: Config.t() | nil
 | 
			
		||||
  def get_by_key(key), do: Repo.get_by(Config, key: key)
 | 
			
		||||
 | 
			
		||||
  @spec changeset(Config.t(), map()) :: Changeset.t()
 | 
			
		||||
  def changeset(config, params \\ %{}) do
 | 
			
		||||
    config
 | 
			
		||||
    |> cast(params, [:key, :value])
 | 
			
		||||
    |> validate_required([:key, :value])
 | 
			
		||||
    |> unique_constraint(:key)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
 | 
			
		||||
  def create(%{key: key, value: value}) do
 | 
			
		||||
    %Config{}
 | 
			
		||||
    |> changeset(%{key: key, value: transform(value)})
 | 
			
		||||
    |> Repo.insert()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec update(Config.t(), map()) :: {:ok, Config} | {:error, Changeset.t()}
 | 
			
		||||
  def update(%Config{} = config, %{value: value}) do
 | 
			
		||||
    config
 | 
			
		||||
    |> change(value: transform(value))
 | 
			
		||||
    |> Repo.update()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec update_or_create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
 | 
			
		||||
  def update_or_create(%{key: key} = params) do
 | 
			
		||||
    with %Config{} = config <- Config.get_by_key(key) do
 | 
			
		||||
      Config.update(config, params)
 | 
			
		||||
    else
 | 
			
		||||
      nil -> Config.create(params)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec delete(String.t()) :: {:ok, Config.t()} | {:error, Changeset.t()}
 | 
			
		||||
  def delete(key) do
 | 
			
		||||
    with %Config{} = config <- Config.get_by_key(key) do
 | 
			
		||||
      Repo.delete(config)
 | 
			
		||||
    else
 | 
			
		||||
      nil -> {:error, "Config with key #{key} not found"}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec from_binary(binary()) :: term()
 | 
			
		||||
  def from_binary(value), do: :erlang.binary_to_term(value)
 | 
			
		||||
 | 
			
		||||
  @spec from_binary_to_map(binary()) :: any()
 | 
			
		||||
  def from_binary_to_map(binary) do
 | 
			
		||||
    from_binary(binary)
 | 
			
		||||
    |> do_convert()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp do_convert([{k, v}] = value) when is_list(value) and length(value) == 1,
 | 
			
		||||
    do: %{k => do_convert(v)}
 | 
			
		||||
 | 
			
		||||
  defp do_convert(values) when is_list(values), do: for(val <- values, do: do_convert(val))
 | 
			
		||||
 | 
			
		||||
  defp do_convert({k, v} = value) when is_tuple(value),
 | 
			
		||||
    do: %{k => do_convert(v)}
 | 
			
		||||
 | 
			
		||||
  defp do_convert(value) when is_binary(value) or is_atom(value) or is_map(value),
 | 
			
		||||
    do: value
 | 
			
		||||
 | 
			
		||||
  @spec transform(any()) :: binary()
 | 
			
		||||
  def transform(entity) when is_map(entity) do
 | 
			
		||||
    tuples =
 | 
			
		||||
      for {k, v} <- entity,
 | 
			
		||||
          into: [],
 | 
			
		||||
          do: {if(is_atom(k), do: k, else: String.to_atom(k)), do_transform(v)}
 | 
			
		||||
 | 
			
		||||
    Enum.reject(tuples, fn {_k, v} -> is_nil(v) end)
 | 
			
		||||
    |> Enum.sort()
 | 
			
		||||
    |> :erlang.term_to_binary()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def transform(entity) when is_list(entity) do
 | 
			
		||||
    list = Enum.map(entity, &do_transform(&1))
 | 
			
		||||
    :erlang.term_to_binary(list)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def transform(entity), do: :erlang.term_to_binary(entity)
 | 
			
		||||
 | 
			
		||||
  defp do_transform(%Regex{} = value) when is_map(value), do: value
 | 
			
		||||
 | 
			
		||||
  defp do_transform(value) when is_map(value) do
 | 
			
		||||
    values =
 | 
			
		||||
      for {key, val} <- value,
 | 
			
		||||
          into: [],
 | 
			
		||||
          do: {String.to_atom(key), do_transform(val)}
 | 
			
		||||
 | 
			
		||||
    Enum.sort(values)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp do_transform(value) when is_list(value) do
 | 
			
		||||
    Enum.map(value, &do_transform(&1))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp do_transform(entity) when is_list(entity) and length(entity) == 1, do: hd(entity)
 | 
			
		||||
 | 
			
		||||
  defp do_transform(value) when is_binary(value) do
 | 
			
		||||
    value = String.trim(value)
 | 
			
		||||
 | 
			
		||||
    case String.length(value) do
 | 
			
		||||
      0 ->
 | 
			
		||||
        nil
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        cond do
 | 
			
		||||
          String.starts_with?(value, "Pleroma") ->
 | 
			
		||||
            String.to_existing_atom("Elixir." <> value)
 | 
			
		||||
 | 
			
		||||
          String.starts_with?(value, ":") ->
 | 
			
		||||
            String.replace(value, ":", "") |> String.to_existing_atom()
 | 
			
		||||
 | 
			
		||||
          String.starts_with?(value, "i:") ->
 | 
			
		||||
            String.replace(value, "i:", "") |> String.to_integer()
 | 
			
		||||
 | 
			
		||||
          true ->
 | 
			
		||||
            value
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp do_transform(value), do: value
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										16
									
								
								lib/pleroma/web/admin_api/views/config_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								lib/pleroma/web/admin_api/views/config_view.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
defmodule Pleroma.Web.AdminAPI.ConfigView do
 | 
			
		||||
  use Pleroma.Web, :view
 | 
			
		||||
 | 
			
		||||
  def render("index.json", %{configs: configs}) do
 | 
			
		||||
    %{
 | 
			
		||||
      configs: render_many(configs, __MODULE__, "show.json", as: :config)
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def render("show.json", %{config: config}) do
 | 
			
		||||
    %{
 | 
			
		||||
      key: config.key,
 | 
			
		||||
      value: Pleroma.Web.AdminAPI.Config.from_binary_to_map(config.value)
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -5,6 +5,7 @@
 | 
			
		|||
defmodule Pleroma.Web.AdminAPI.ReportView do
 | 
			
		||||
  use Pleroma.Web, :view
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.HTML
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Web.CommonAPI.Utils
 | 
			
		||||
  alias Pleroma.Web.MastodonAPI.AccountView
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +24,13 @@ def render("show.json", %{report: report}) do
 | 
			
		|||
    [account_ap_id | status_ap_ids] = report.data["object"]
 | 
			
		||||
    account = User.get_cached_by_ap_id(account_ap_id)
 | 
			
		||||
 | 
			
		||||
    content =
 | 
			
		||||
      unless is_nil(report.data["content"]) do
 | 
			
		||||
        HTML.filter_tags(report.data["content"])
 | 
			
		||||
      else
 | 
			
		||||
        nil
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    statuses =
 | 
			
		||||
      Enum.map(status_ap_ids, fn ap_id ->
 | 
			
		||||
        Activity.get_by_ap_id_with_object(ap_id)
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +40,7 @@ def render("show.json", %{report: report}) do
 | 
			
		|||
      id: report.id,
 | 
			
		||||
      account: AccountView.render("account.json", %{user: account}),
 | 
			
		||||
      actor: AccountView.render("account.json", %{user: user}),
 | 
			
		||||
      content: report.data["content"],
 | 
			
		||||
      content: content,
 | 
			
		||||
      created_at: created_at,
 | 
			
		||||
      statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),
 | 
			
		||||
      state: report.data["state"]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,14 @@ def get_user(%Plug.Conn{} = conn) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Gets or creates Pleroma.Registration record from Ueberauth assigns.
 | 
			
		||||
  Note: some strategies (like `keycloak`) might need extra configuration to fill `uid` from callback response —
 | 
			
		||||
    see [`docs/config.md`](docs/config.md).
 | 
			
		||||
  """
 | 
			
		||||
  def get_registration(%Plug.Conn{assigns: %{ueberauth_auth: %{uid: nil}}}),
 | 
			
		||||
    do: {:error, :missing_uid}
 | 
			
		||||
 | 
			
		||||
  def get_registration(%Plug.Conn{
 | 
			
		||||
        assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}
 | 
			
		||||
      }) do
 | 
			
		||||
| 
						 | 
				
			
			@ -51,9 +59,10 @@ def get_registration(%Plug.Conn{
 | 
			
		|||
 | 
			
		||||
  def get_registration(%Plug.Conn{} = _conn), do: {:error, :missing_credentials}
 | 
			
		||||
 | 
			
		||||
  @doc "Creates Pleroma.User record basing on params and Pleroma.Registration record."
 | 
			
		||||
  def create_from_registration(
 | 
			
		||||
        %Plug.Conn{params: %{"authorization" => registration_attrs}},
 | 
			
		||||
        registration
 | 
			
		||||
        %Registration{} = registration
 | 
			
		||||
      ) do
 | 
			
		||||
    nickname = value([registration_attrs["nickname"], Registration.nickname(registration)])
 | 
			
		||||
    email = value([registration_attrs["email"], Registration.email(registration)])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,9 +35,9 @@ def unfollow(follower, unfollowed) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def accept_follow_request(follower, followed) do
 | 
			
		||||
    with {:ok, follower} <- User.maybe_follow(follower, followed),
 | 
			
		||||
    with {:ok, follower} <- User.follow(follower, followed),
 | 
			
		||||
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
 | 
			
		||||
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
 | 
			
		||||
         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
 | 
			
		||||
         {:ok, _activity} <-
 | 
			
		||||
           ActivityPub.accept(%{
 | 
			
		||||
             to: [follower.ap_id],
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +51,7 @@ def accept_follow_request(follower, followed) do
 | 
			
		|||
 | 
			
		||||
  def reject_follow_request(follower, followed) do
 | 
			
		||||
    with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
 | 
			
		||||
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
 | 
			
		||||
         {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
 | 
			
		||||
         {:ok, _activity} <-
 | 
			
		||||
           ActivityPub.reject(%{
 | 
			
		||||
             to: [follower.ap_id],
 | 
			
		||||
| 
						 | 
				
			
			@ -119,6 +119,56 @@ def unfavorite(id_or_ap_id, user) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def vote(user, object, choices) do
 | 
			
		||||
    with "Question" <- object.data["type"],
 | 
			
		||||
         {:author, false} <- {:author, object.data["actor"] == user.ap_id},
 | 
			
		||||
         {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
 | 
			
		||||
         {options, max_count} <- get_options_and_max_count(object),
 | 
			
		||||
         option_count <- Enum.count(options),
 | 
			
		||||
         {:choice_check, {choices, true}} <-
 | 
			
		||||
           {:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
 | 
			
		||||
         {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} 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}
 | 
			
		||||
    else
 | 
			
		||||
      {:author, _} -> {:error, "Poll's author can't vote"}
 | 
			
		||||
      {:existing_votes, _} -> {:error, "Already voted"}
 | 
			
		||||
      {:choice_check, {_, false}} -> {:error, "Invalid indices"}
 | 
			
		||||
      {:count_check, false} -> {:error, "Too many choices"}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_options_and_max_count(object) do
 | 
			
		||||
    if Map.has_key?(object.data, "anyOf") do
 | 
			
		||||
      {object.data["anyOf"], Enum.count(object.data["anyOf"])}
 | 
			
		||||
    else
 | 
			
		||||
      {object.data["oneOf"], 1}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp normalize_and_validate_choice_indices(choices, count) do
 | 
			
		||||
    Enum.map_reduce(choices, true, fn index, valid ->
 | 
			
		||||
      index = if is_binary(index), do: String.to_integer(index), else: index
 | 
			
		||||
      {index, if(valid, do: index < count, else: valid)}
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  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)}
 | 
			
		||||
| 
						 | 
				
			
			@ -154,12 +204,15 @@ def post(user, %{"status" => status} = data) do
 | 
			
		|||
             data,
 | 
			
		||||
             visibility
 | 
			
		||||
           ),
 | 
			
		||||
         {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
 | 
			
		||||
         mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
 | 
			
		||||
         addressed_users <- get_addressed_users(mentioned_users, data["to"]),
 | 
			
		||||
         {poll, poll_emoji} <- make_poll_data(data),
 | 
			
		||||
         {to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility),
 | 
			
		||||
         context <- make_context(in_reply_to),
 | 
			
		||||
         cw <- data["spoiler_text"] || "",
 | 
			
		||||
         sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
 | 
			
		||||
         full_payload <- String.trim(status <> cw),
 | 
			
		||||
         length when length in 1..limit <- String.length(full_payload),
 | 
			
		||||
         :ok <- validate_character_limit(full_payload, attachments, limit),
 | 
			
		||||
         object <-
 | 
			
		||||
           make_note_data(
 | 
			
		||||
             user.ap_id,
 | 
			
		||||
| 
						 | 
				
			
			@ -171,13 +224,14 @@ def post(user, %{"status" => status} = data) do
 | 
			
		|||
             tags,
 | 
			
		||||
             cw,
 | 
			
		||||
             cc,
 | 
			
		||||
             sensitive
 | 
			
		||||
             sensitive,
 | 
			
		||||
             poll
 | 
			
		||||
           ),
 | 
			
		||||
         object <-
 | 
			
		||||
           Map.put(
 | 
			
		||||
             object,
 | 
			
		||||
             "emoji",
 | 
			
		||||
             Formatter.get_emoji_map(full_payload)
 | 
			
		||||
             Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
 | 
			
		||||
           ) do
 | 
			
		||||
      res =
 | 
			
		||||
        ActivityPub.create(
 | 
			
		||||
| 
						 | 
				
			
			@ -193,6 +247,7 @@ def post(user, %{"status" => status} = data) do
 | 
			
		|||
 | 
			
		||||
      res
 | 
			
		||||
    else
 | 
			
		||||
      {:error, _} = e -> e
 | 
			
		||||
      e -> {:error, e}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,9 +61,9 @@ def attachments_from_ids_descs(ids, descs_str) do
 | 
			
		|||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
 | 
			
		||||
    mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
 | 
			
		||||
 | 
			
		||||
  @spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) ::
 | 
			
		||||
          {list(String.t()), list(String.t())}
 | 
			
		||||
  def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do
 | 
			
		||||
    to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users]
 | 
			
		||||
    cc = [user.follower_address]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -74,9 +74,7 @@ def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
 | 
			
		||||
    mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
 | 
			
		||||
 | 
			
		||||
  def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do
 | 
			
		||||
    to = [user.follower_address | mentioned_users]
 | 
			
		||||
    cc = ["https://www.w3.org/ns/activitystreams#Public"]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -87,14 +85,12 @@ def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
 | 
			
		||||
    {to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "direct")
 | 
			
		||||
  def get_to_and_cc(user, mentioned_users, inReplyTo, "private") do
 | 
			
		||||
    {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct")
 | 
			
		||||
    {[user.follower_address | to], cc}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do
 | 
			
		||||
    mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
 | 
			
		||||
 | 
			
		||||
  def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do
 | 
			
		||||
    if inReplyTo do
 | 
			
		||||
      {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
 | 
			
		||||
    else
 | 
			
		||||
| 
						 | 
				
			
			@ -102,6 +98,78 @@ def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_addressed_users(_, to) when is_list(to) do
 | 
			
		||||
    User.get_ap_ids_by_nicknames(to)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_addressed_users(mentioned_users, _), do: mentioned_users
 | 
			
		||||
 | 
			
		||||
  def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
 | 
			
		||||
      when is_list(options) do
 | 
			
		||||
    %{max_expiration: max_expiration, min_expiration: min_expiration} =
 | 
			
		||||
      limits = Pleroma.Config.get([:instance, :poll_limits])
 | 
			
		||||
 | 
			
		||||
    # XXX: There is probably a cleaner way of doing this
 | 
			
		||||
    try do
 | 
			
		||||
      # In some cases mastofe sends out strings instead of integers
 | 
			
		||||
      expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in
 | 
			
		||||
 | 
			
		||||
      if Enum.count(options) > limits.max_options do
 | 
			
		||||
        raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options"
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      {poll, emoji} =
 | 
			
		||||
        Enum.map_reduce(options, %{}, fn option, emoji ->
 | 
			
		||||
          if String.length(option) > limits.max_option_chars do
 | 
			
		||||
            raise ArgumentError,
 | 
			
		||||
              message:
 | 
			
		||||
                "Poll options cannot be longer than #{limits.max_option_chars} characters each"
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          {%{
 | 
			
		||||
             "name" => option,
 | 
			
		||||
             "type" => "Note",
 | 
			
		||||
             "replies" => %{"type" => "Collection", "totalItems" => 0}
 | 
			
		||||
           }, Map.merge(emoji, Formatter.get_emoji_map(option))}
 | 
			
		||||
        end)
 | 
			
		||||
 | 
			
		||||
      case expires_in do
 | 
			
		||||
        expires_in when expires_in > max_expiration ->
 | 
			
		||||
          raise ArgumentError, message: "Expiration date is too far in the future"
 | 
			
		||||
 | 
			
		||||
        expires_in when expires_in < min_expiration ->
 | 
			
		||||
          raise ArgumentError, message: "Expiration date is too soon"
 | 
			
		||||
 | 
			
		||||
        _ ->
 | 
			
		||||
          :noop
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      end_time =
 | 
			
		||||
        NaiveDateTime.utc_now()
 | 
			
		||||
        |> NaiveDateTime.add(expires_in)
 | 
			
		||||
        |> NaiveDateTime.to_iso8601()
 | 
			
		||||
 | 
			
		||||
      poll =
 | 
			
		||||
        if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do
 | 
			
		||||
          %{"type" => "Question", "anyOf" => poll, "closed" => end_time}
 | 
			
		||||
        else
 | 
			
		||||
          %{"type" => "Question", "oneOf" => poll, "closed" => end_time}
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      {poll, emoji}
 | 
			
		||||
    rescue
 | 
			
		||||
      e in ArgumentError -> e.message
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def make_poll_data(%{"poll" => poll}) when is_map(poll) do
 | 
			
		||||
    "Invalid poll"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def make_poll_data(_data) do
 | 
			
		||||
    {%{}, %{}}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def make_content_html(
 | 
			
		||||
        status,
 | 
			
		||||
        attachments,
 | 
			
		||||
| 
						 | 
				
			
			@ -224,7 +292,8 @@ def make_note_data(
 | 
			
		|||
        tags,
 | 
			
		||||
        cw \\ nil,
 | 
			
		||||
        cc \\ [],
 | 
			
		||||
        sensitive \\ false
 | 
			
		||||
        sensitive \\ false,
 | 
			
		||||
        merge \\ %{}
 | 
			
		||||
      ) do
 | 
			
		||||
    object = %{
 | 
			
		||||
      "type" => "Note",
 | 
			
		||||
| 
						 | 
				
			
			@ -239,12 +308,15 @@ def make_note_data(
 | 
			
		|||
      "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    with false <- is_nil(in_reply_to),
 | 
			
		||||
         %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
 | 
			
		||||
      Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
 | 
			
		||||
    else
 | 
			
		||||
      _ -> object
 | 
			
		||||
    end
 | 
			
		||||
    object =
 | 
			
		||||
      with false <- is_nil(in_reply_to),
 | 
			
		||||
           %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
 | 
			
		||||
        Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
 | 
			
		||||
      else
 | 
			
		||||
        _ -> object
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    Map.merge(object, merge)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def format_naive_asctime(date) do
 | 
			
		||||
| 
						 | 
				
			
			@ -421,4 +493,29 @@ def conversation_id_to_context(id) do
 | 
			
		|||
        {:error, "No such conversation"}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def make_answer_data(%User{ap_id: ap_id}, object, name) do
 | 
			
		||||
    %{
 | 
			
		||||
      "type" => "Answer",
 | 
			
		||||
      "actor" => ap_id,
 | 
			
		||||
      "cc" => [object.data["actor"]],
 | 
			
		||||
      "to" => [],
 | 
			
		||||
      "name" => name,
 | 
			
		||||
      "inReplyTo" => object.data["id"]
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def validate_character_limit(full_payload, attachments, limit) do
 | 
			
		||||
    length = String.length(full_payload)
 | 
			
		||||
 | 
			
		||||
    if length < limit do
 | 
			
		||||
      if length > 0 or Enum.count(attachments) > 0 do
 | 
			
		||||
        :ok
 | 
			
		||||
      else
 | 
			
		||||
        {:error, "Cannot post an empty status without attachments"}
 | 
			
		||||
      end
 | 
			
		||||
    else
 | 
			
		||||
      {:error, "The status is over the character limit"}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,4 +15,22 @@ def json_response(conn, status, json) do
 | 
			
		|||
    |> put_status(status)
 | 
			
		||||
    |> json(json)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil
 | 
			
		||||
  def fetch_integer_param(params, name, default \\ nil) do
 | 
			
		||||
    params
 | 
			
		||||
    |> Map.get(name, default)
 | 
			
		||||
    |> param_to_integer(default)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp param_to_integer(val, _) when is_integer(val), do: val
 | 
			
		||||
 | 
			
		||||
  defp param_to_integer(val, default) when is_binary(val) do
 | 
			
		||||
    case Integer.parse(val) do
 | 
			
		||||
      {res, _} -> res
 | 
			
		||||
      _ -> default
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp param_to_integer(_, default), do: default
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,17 +16,32 @@ defmodule Pleroma.Web.Endpoint do
 | 
			
		|||
 | 
			
		||||
  plug(Pleroma.Plugs.UploadedMedia)
 | 
			
		||||
 | 
			
		||||
  @static_cache_control "public, no-cache"
 | 
			
		||||
 | 
			
		||||
  # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
 | 
			
		||||
  # If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
 | 
			
		||||
  plug(Pleroma.Plugs.InstanceStatic, at: "/")
 | 
			
		||||
  # Cache-control headers are duplicated in case we turn off etags in the future
 | 
			
		||||
  plug(Pleroma.Plugs.InstanceStatic,
 | 
			
		||||
    at: "/",
 | 
			
		||||
    gzip: true,
 | 
			
		||||
    cache_control_for_etags: @static_cache_control,
 | 
			
		||||
    headers: %{
 | 
			
		||||
      "cache-control" => @static_cache_control
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  plug(
 | 
			
		||||
    Plug.Static,
 | 
			
		||||
    at: "/",
 | 
			
		||||
    from: :pleroma,
 | 
			
		||||
    only:
 | 
			
		||||
      ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
 | 
			
		||||
      ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc),
 | 
			
		||||
    # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
 | 
			
		||||
    gzip: true,
 | 
			
		||||
    cache_control_for_etags: @static_cache_control,
 | 
			
		||||
    headers: %{
 | 
			
		||||
      "cache-control" => @static_cache_control
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  plug(Plug.Static.IndexHtml, at: "/pleroma/admin/")
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +66,7 @@ defmodule Pleroma.Web.Endpoint do
 | 
			
		|||
    parsers: [:urlencoded, :multipart, :json],
 | 
			
		||||
    pass: ["*/*"],
 | 
			
		||||
    json_decoder: Jason,
 | 
			
		||||
    length: Application.get_env(:pleroma, :instance) |> Keyword.get(:upload_limit),
 | 
			
		||||
    length: Pleroma.Config.get([:instance, :upload_limit]),
 | 
			
		||||
    body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +91,7 @@ defmodule Pleroma.Web.Endpoint do
 | 
			
		|||
    Plug.Session,
 | 
			
		||||
    store: :cookie,
 | 
			
		||||
    key: cookie_name,
 | 
			
		||||
    signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},
 | 
			
		||||
    signing_salt: Pleroma.Config.get([__MODULE__, :signing_salt], "CqaoopA2"),
 | 
			
		||||
    http_only: true,
 | 
			
		||||
    secure: secure_cookies,
 | 
			
		||||
    extra: extra
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,13 +11,11 @@ defmodule Pleroma.Web.Federator do
 | 
			
		|||
  alias Pleroma.Web.ActivityPub.Utils
 | 
			
		||||
  alias Pleroma.Web.Federator.Publisher
 | 
			
		||||
  alias Pleroma.Web.Federator.RetryQueue
 | 
			
		||||
  alias Pleroma.Web.OStatus
 | 
			
		||||
  alias Pleroma.Web.Websub
 | 
			
		||||
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  @websub Application.get_env(:pleroma, :websub)
 | 
			
		||||
  @ostatus Application.get_env(:pleroma, :ostatus)
 | 
			
		||||
 | 
			
		||||
  def init do
 | 
			
		||||
    # 1 minute
 | 
			
		||||
    Process.sleep(1000 * 60)
 | 
			
		||||
| 
						 | 
				
			
			@ -87,12 +85,12 @@ def perform(:verify_websub, websub) do
 | 
			
		|||
      "Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})"
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    @websub.verify(websub)
 | 
			
		||||
    Websub.verify(websub)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def perform(:incoming_doc, doc) do
 | 
			
		||||
    Logger.info("Got document, trying to parse")
 | 
			
		||||
    @ostatus.handle_incoming(doc)
 | 
			
		||||
    OStatus.handle_incoming(doc)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def perform(:incoming_ap_doc, params) do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,9 @@ def init(args) do
 | 
			
		|||
 | 
			
		||||
  def start_link do
 | 
			
		||||
    enabled =
 | 
			
		||||
      if Mix.env() == :test, do: true, else: Pleroma.Config.get([__MODULE__, :enabled], false)
 | 
			
		||||
      if Pleroma.Config.get(:env) == :test,
 | 
			
		||||
        do: true,
 | 
			
		||||
        else: Pleroma.Config.get([__MODULE__, :enabled], false)
 | 
			
		||||
 | 
			
		||||
    if enabled do
 | 
			
		||||
      Logger.info("Starting retry queue")
 | 
			
		||||
| 
						 | 
				
			
			@ -219,7 +221,7 @@ def handle_info(unknown, state) do
 | 
			
		|||
    {:noreply, state}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  if Mix.env() == :test do
 | 
			
		||||
  if Pleroma.Config.get(:env) == :test do
 | 
			
		||||
    defp growth_function(_retries) do
 | 
			
		||||
      _shutit = Pleroma.Config.get([__MODULE__, :initial_timeout])
 | 
			
		||||
      DateTime.to_unix(DateTime.utc_now()) - 1
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,9 +11,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 | 
			
		|||
  alias Pleroma.Conversation.Participation
 | 
			
		||||
  alias Pleroma.Filter
 | 
			
		||||
  alias Pleroma.Formatter
 | 
			
		||||
  alias Pleroma.HTTP
 | 
			
		||||
  alias Pleroma.Notification
 | 
			
		||||
  alias Pleroma.Object
 | 
			
		||||
  alias Pleroma.Object.Fetcher
 | 
			
		||||
  alias Pleroma.Pagination
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
  alias Pleroma.ScheduledActivity
 | 
			
		||||
| 
						 | 
				
			
			@ -46,16 +46,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 | 
			
		|||
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  plug(
 | 
			
		||||
    Pleroma.Plugs.RateLimitPlug,
 | 
			
		||||
    %{
 | 
			
		||||
      max_requests: Config.get([:app_account_creation, :max_requests]),
 | 
			
		||||
      interval: Config.get([:app_account_creation, :interval])
 | 
			
		||||
    }
 | 
			
		||||
    when action in [:account_register]
 | 
			
		||||
  )
 | 
			
		||||
  plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register)
 | 
			
		||||
  plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
 | 
			
		||||
 | 
			
		||||
  @httpoison Application.get_env(:pleroma, :httpoison)
 | 
			
		||||
  @local_mastodon_name "Mastodon-Local"
 | 
			
		||||
 | 
			
		||||
  action_fallback(:errors)
 | 
			
		||||
| 
						 | 
				
			
			@ -117,13 +110,24 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
 | 
			
		|||
      |> Enum.dedup()
 | 
			
		||||
 | 
			
		||||
    info_params =
 | 
			
		||||
      [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
 | 
			
		||||
      [
 | 
			
		||||
        :no_rich_text,
 | 
			
		||||
        :locked,
 | 
			
		||||
        :hide_followers,
 | 
			
		||||
        :hide_follows,
 | 
			
		||||
        :hide_favorites,
 | 
			
		||||
        :show_role,
 | 
			
		||||
        :skip_thread_containment
 | 
			
		||||
      ]
 | 
			
		||||
      |> Enum.reduce(%{}, fn key, acc ->
 | 
			
		||||
        add_if_present(acc, params, to_string(key), key, fn value ->
 | 
			
		||||
          {:ok, ControllerHelper.truthy_param?(value)}
 | 
			
		||||
        end)
 | 
			
		||||
      end)
 | 
			
		||||
      |> add_if_present(params, "default_scope", :default_scope)
 | 
			
		||||
      |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
 | 
			
		||||
        {:ok, Map.merge(user.info.pleroma_settings_store, value)}
 | 
			
		||||
      end)
 | 
			
		||||
      |> add_if_present(params, "header", :banner, fn value ->
 | 
			
		||||
        with %Plug.Upload{} <- value,
 | 
			
		||||
             {:ok, object} <- ActivityPub.upload(value, type: :banner) do
 | 
			
		||||
| 
						 | 
				
			
			@ -132,6 +136,14 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
 | 
			
		|||
          _ -> :error
 | 
			
		||||
        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}
 | 
			
		||||
        else
 | 
			
		||||
          _ -> :error
 | 
			
		||||
        end
 | 
			
		||||
      end)
 | 
			
		||||
      |> Map.put(:emoji, user_info_emojis)
 | 
			
		||||
 | 
			
		||||
    info_cng = User.Info.profile_update(user.info, info_params)
 | 
			
		||||
| 
						 | 
				
			
			@ -143,7 +155,10 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
 | 
			
		|||
        CommonAPI.update(user)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      json(conn, AccountView.render("account.json", %{user: user, for: user}))
 | 
			
		||||
      json(
 | 
			
		||||
        conn,
 | 
			
		||||
        AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
 | 
			
		||||
      )
 | 
			
		||||
    else
 | 
			
		||||
      _e ->
 | 
			
		||||
        conn
 | 
			
		||||
| 
						 | 
				
			
			@ -216,7 +231,16 @@ def update_background(%{assigns: %{user: user}} = conn, params) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
 | 
			
		||||
    account = AccountView.render("account.json", %{user: user, for: user})
 | 
			
		||||
    chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
 | 
			
		||||
 | 
			
		||||
    account =
 | 
			
		||||
      AccountView.render("account.json", %{
 | 
			
		||||
        user: user,
 | 
			
		||||
        for: user,
 | 
			
		||||
        with_pleroma_settings: true,
 | 
			
		||||
        with_chat_token: chat_token
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    json(conn, account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -260,7 +284,8 @@ def masto_instance(conn, _params) do
 | 
			
		|||
      languages: ["en"],
 | 
			
		||||
      registrations: Pleroma.Config.get([:instance, :registrations_open]),
 | 
			
		||||
      # Extra (not present in Mastodon):
 | 
			
		||||
      max_toot_chars: Keyword.get(instance, :limit)
 | 
			
		||||
      max_toot_chars: Keyword.get(instance, :limit),
 | 
			
		||||
      poll_limits: Keyword.get(instance, :poll_limits)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    json(conn, response)
 | 
			
		||||
| 
						 | 
				
			
			@ -472,6 +497,67 @@ def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
 | 
			
		||||
    with %Object{} = object <- Object.get_by_id(id),
 | 
			
		||||
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
 | 
			
		||||
         true <- Visibility.visible_for_user?(activity, user) do
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_view(StatusView)
 | 
			
		||||
      |> try_render("poll.json", %{object: object, for: user})
 | 
			
		||||
    else
 | 
			
		||||
      nil ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_status(404)
 | 
			
		||||
        |> json(%{error: "Record not found"})
 | 
			
		||||
 | 
			
		||||
      false ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_status(404)
 | 
			
		||||
        |> json(%{error: "Record not found"})
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_cached_vote_or_vote(user, object, choices) do
 | 
			
		||||
    idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
 | 
			
		||||
 | 
			
		||||
    {_, res} =
 | 
			
		||||
      Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
 | 
			
		||||
        case CommonAPI.vote(user, object, choices) do
 | 
			
		||||
          {:error, _message} = res -> {:ignore, res}
 | 
			
		||||
          res -> {:commit, res}
 | 
			
		||||
        end
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    res
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
 | 
			
		||||
    with %Object{} = object <- Object.get_by_id(id),
 | 
			
		||||
         true <- object.data["type"] == "Question",
 | 
			
		||||
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
 | 
			
		||||
         true <- Visibility.visible_for_user?(activity, user),
 | 
			
		||||
         {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_view(StatusView)
 | 
			
		||||
      |> try_render("poll.json", %{object: object, for: user})
 | 
			
		||||
    else
 | 
			
		||||
      nil ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_status(404)
 | 
			
		||||
        |> json(%{error: "Record not found"})
 | 
			
		||||
 | 
			
		||||
      false ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_status(404)
 | 
			
		||||
        |> json(%{error: "Record not found"})
 | 
			
		||||
 | 
			
		||||
      {:error, message} ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_status(422)
 | 
			
		||||
        |> json(%{error: message})
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
 | 
			
		||||
    with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
 | 
			
		||||
      conn
 | 
			
		||||
| 
						 | 
				
			
			@ -521,26 +607,11 @@ def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => schedule
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
 | 
			
		||||
      when length(media_ids) > 0 do
 | 
			
		||||
    params =
 | 
			
		||||
      params
 | 
			
		||||
      |> Map.put("status", ".")
 | 
			
		||||
 | 
			
		||||
    post_status(conn, params)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
 | 
			
		||||
    params =
 | 
			
		||||
      params
 | 
			
		||||
      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
 | 
			
		||||
 | 
			
		||||
    idempotency_key =
 | 
			
		||||
      case get_req_header(conn, "idempotency-key") do
 | 
			
		||||
        [key] -> key
 | 
			
		||||
        _ -> Ecto.UUID.generate()
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    scheduled_at = params["scheduled_at"]
 | 
			
		||||
 | 
			
		||||
    if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
 | 
			
		||||
| 
						 | 
				
			
			@ -553,17 +624,40 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
 | 
			
		|||
    else
 | 
			
		||||
      params = Map.drop(params, ["scheduled_at"])
 | 
			
		||||
 | 
			
		||||
      {:ok, activity} =
 | 
			
		||||
        Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
 | 
			
		||||
          CommonAPI.post(user, params)
 | 
			
		||||
        end)
 | 
			
		||||
      case get_cached_status_or_post(conn, params) do
 | 
			
		||||
        {:ignore, message} ->
 | 
			
		||||
          conn
 | 
			
		||||
          |> put_status(422)
 | 
			
		||||
          |> json(%{error: message})
 | 
			
		||||
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_view(StatusView)
 | 
			
		||||
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
 | 
			
		||||
        {:error, message} ->
 | 
			
		||||
          conn
 | 
			
		||||
          |> put_status(422)
 | 
			
		||||
          |> json(%{error: message})
 | 
			
		||||
 | 
			
		||||
        {_, activity} ->
 | 
			
		||||
          conn
 | 
			
		||||
          |> put_view(StatusView)
 | 
			
		||||
          |> try_render("status.json", %{activity: activity, for: user, as: :activity})
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
 | 
			
		||||
    idempotency_key =
 | 
			
		||||
      case get_req_header(conn, "idempotency-key") do
 | 
			
		||||
        [key] -> key
 | 
			
		||||
        _ -> Ecto.UUID.generate()
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
 | 
			
		||||
      case CommonAPI.post(user, params) do
 | 
			
		||||
        {:ok, activity} -> activity
 | 
			
		||||
        {:error, message} -> {:ignore, message}
 | 
			
		||||
      end
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
 | 
			
		||||
    with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
 | 
			
		||||
      json(conn, %{})
 | 
			
		||||
| 
						 | 
				
			
			@ -1107,114 +1201,6 @@ def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def status_search_query_with_gin(q, query) do
 | 
			
		||||
    from([a, o] in q,
 | 
			
		||||
      where:
 | 
			
		||||
        fragment(
 | 
			
		||||
          "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
 | 
			
		||||
          o.data,
 | 
			
		||||
          ^query
 | 
			
		||||
        ),
 | 
			
		||||
      order_by: [desc: :id]
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def status_search_query_with_rum(q, query) do
 | 
			
		||||
    from([a, o] in q,
 | 
			
		||||
      where:
 | 
			
		||||
        fragment(
 | 
			
		||||
          "? @@ plainto_tsquery('english', ?)",
 | 
			
		||||
          o.fts_content,
 | 
			
		||||
          ^query
 | 
			
		||||
        ),
 | 
			
		||||
      order_by: [fragment("? <=> now()::date", o.inserted_at)]
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def status_search(user, query) do
 | 
			
		||||
    fetched =
 | 
			
		||||
      if Regex.match?(~r/https?:/, query) do
 | 
			
		||||
        with {:ok, object} <- Fetcher.fetch_object_from_id(query),
 | 
			
		||||
             %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
 | 
			
		||||
             true <- Visibility.visible_for_user?(activity, user) do
 | 
			
		||||
          [activity]
 | 
			
		||||
        else
 | 
			
		||||
          _e -> []
 | 
			
		||||
        end
 | 
			
		||||
      end || []
 | 
			
		||||
 | 
			
		||||
    q =
 | 
			
		||||
      from([a, o] in Activity.with_preloaded_object(Activity),
 | 
			
		||||
        where: fragment("?->>'type' = 'Create'", a.data),
 | 
			
		||||
        where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
 | 
			
		||||
        limit: 20
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    q =
 | 
			
		||||
      if Pleroma.Config.get([:database, :rum_enabled]) do
 | 
			
		||||
        status_search_query_with_rum(q, query)
 | 
			
		||||
      else
 | 
			
		||||
        status_search_query_with_gin(q, query)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    Repo.all(q) ++ fetched
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
 | 
			
		||||
    accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
 | 
			
		||||
 | 
			
		||||
    statuses = status_search(user, query)
 | 
			
		||||
 | 
			
		||||
    tags_path = Web.base_url() <> "/tag/"
 | 
			
		||||
 | 
			
		||||
    tags =
 | 
			
		||||
      query
 | 
			
		||||
      |> String.split()
 | 
			
		||||
      |> Enum.uniq()
 | 
			
		||||
      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
 | 
			
		||||
      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
 | 
			
		||||
      |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
 | 
			
		||||
 | 
			
		||||
    res = %{
 | 
			
		||||
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
 | 
			
		||||
      "statuses" =>
 | 
			
		||||
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
 | 
			
		||||
      "hashtags" => tags
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    json(conn, res)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
 | 
			
		||||
    accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
 | 
			
		||||
 | 
			
		||||
    statuses = status_search(user, query)
 | 
			
		||||
 | 
			
		||||
    tags =
 | 
			
		||||
      query
 | 
			
		||||
      |> String.split()
 | 
			
		||||
      |> Enum.uniq()
 | 
			
		||||
      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
 | 
			
		||||
      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
 | 
			
		||||
 | 
			
		||||
    res = %{
 | 
			
		||||
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
 | 
			
		||||
      "statuses" =>
 | 
			
		||||
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
 | 
			
		||||
      "hashtags" => tags
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    json(conn, res)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
 | 
			
		||||
    accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
 | 
			
		||||
 | 
			
		||||
    res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
 | 
			
		||||
 | 
			
		||||
    json(conn, res)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def favourites(%{assigns: %{user: user}} = conn, params) do
 | 
			
		||||
    params =
 | 
			
		||||
      params
 | 
			
		||||
| 
						 | 
				
			
			@ -1409,8 +1395,6 @@ def index(%{assigns: %{user: user}} = conn, _params) do
 | 
			
		|||
      accounts =
 | 
			
		||||
        Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
 | 
			
		||||
 | 
			
		||||
      flavour = get_user_flavour(user)
 | 
			
		||||
 | 
			
		||||
      initial_state =
 | 
			
		||||
        %{
 | 
			
		||||
          meta: %{
 | 
			
		||||
| 
						 | 
				
			
			@ -1429,6 +1413,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do
 | 
			
		|||
            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)
 | 
			
		||||
| 
						 | 
				
			
			@ -1496,7 +1481,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do
 | 
			
		|||
      conn
 | 
			
		||||
      |> put_layout(false)
 | 
			
		||||
      |> put_view(MastodonView)
 | 
			
		||||
      |> render("index.html", %{initial_state: initial_state, flavour: flavour})
 | 
			
		||||
      |> render("index.html", %{initial_state: initial_state})
 | 
			
		||||
    else
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_session(:return_to, conn.request_path)
 | 
			
		||||
| 
						 | 
				
			
			@ -1519,43 +1504,6 @@ def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _para
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @supported_flavours ["glitch", "vanilla"]
 | 
			
		||||
 | 
			
		||||
  def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
 | 
			
		||||
      when flavour in @supported_flavours do
 | 
			
		||||
    flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
 | 
			
		||||
 | 
			
		||||
    with changeset <- Ecto.Changeset.change(user),
 | 
			
		||||
         changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
 | 
			
		||||
         {:ok, user} <- User.update_and_set_cache(changeset),
 | 
			
		||||
         flavour <- user.info.flavour do
 | 
			
		||||
      json(conn, flavour)
 | 
			
		||||
    else
 | 
			
		||||
      e ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_resp_content_type("application/json")
 | 
			
		||||
        |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_flavour(conn, _params) do
 | 
			
		||||
    conn
 | 
			
		||||
    |> put_status(400)
 | 
			
		||||
    |> json(%{error: "Unsupported flavour"})
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_flavour(%{assigns: %{user: user}} = conn, _params) do
 | 
			
		||||
    json(conn, get_user_flavour(user))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
 | 
			
		||||
    flavour
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_user_flavour(_) do
 | 
			
		||||
    "glitch"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def login(%{assigns: %{user: %User{}}} = conn, _params) do
 | 
			
		||||
    redirect(conn, to: local_mastodon_root_path(conn))
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -1754,7 +1702,7 @@ def suggestions(%{assigns: %{user: user}} = conn, _) do
 | 
			
		|||
        |> String.replace("{{user}}", user)
 | 
			
		||||
 | 
			
		||||
      with {:ok, %{status: 200, body: body}} <-
 | 
			
		||||
             @httpoison.get(
 | 
			
		||||
             HTTP.get(
 | 
			
		||||
               url,
 | 
			
		||||
               [],
 | 
			
		||||
               adapter: [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										79
									
								
								lib/pleroma/web/mastodon_api/search_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								lib/pleroma/web/mastodon_api/search_controller.ex
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,79 @@
 | 
			
		|||
# 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.SearchController do
 | 
			
		||||
  use Pleroma.Web, :controller
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Web
 | 
			
		||||
  alias Pleroma.Web.MastodonAPI.AccountView
 | 
			
		||||
  alias Pleroma.Web.MastodonAPI.StatusView
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Web.ControllerHelper
 | 
			
		||||
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
 | 
			
		||||
 | 
			
		||||
  def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
 | 
			
		||||
    accounts = User.search(query, search_options(params, user))
 | 
			
		||||
    statuses = Activity.search(user, query)
 | 
			
		||||
    tags_path = Web.base_url() <> "/tag/"
 | 
			
		||||
 | 
			
		||||
    tags =
 | 
			
		||||
      query
 | 
			
		||||
      |> String.split()
 | 
			
		||||
      |> Enum.uniq()
 | 
			
		||||
      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
 | 
			
		||||
      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
 | 
			
		||||
      |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
 | 
			
		||||
 | 
			
		||||
    res = %{
 | 
			
		||||
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
 | 
			
		||||
      "statuses" =>
 | 
			
		||||
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
 | 
			
		||||
      "hashtags" => tags
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    json(conn, res)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
 | 
			
		||||
    accounts = User.search(query, search_options(params, user))
 | 
			
		||||
    statuses = Activity.search(user, query)
 | 
			
		||||
 | 
			
		||||
    tags =
 | 
			
		||||
      query
 | 
			
		||||
      |> String.split()
 | 
			
		||||
      |> Enum.uniq()
 | 
			
		||||
      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
 | 
			
		||||
      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
 | 
			
		||||
 | 
			
		||||
    res = %{
 | 
			
		||||
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
 | 
			
		||||
      "statuses" =>
 | 
			
		||||
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
 | 
			
		||||
      "hashtags" => tags
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    json(conn, res)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
 | 
			
		||||
    accounts = User.search(query, search_options(params, user))
 | 
			
		||||
    res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
 | 
			
		||||
 | 
			
		||||
    json(conn, res)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp search_options(params, user) do
 | 
			
		||||
    [
 | 
			
		||||
      resolve: params["resolve"] == "true",
 | 
			
		||||
      following: params["following"] == "true",
 | 
			
		||||
      limit: ControllerHelper.fetch_integer_param(params, "limit"),
 | 
			
		||||
      offset: ControllerHelper.fetch_integer_param(params, "offset"),
 | 
			
		||||
      for_user: user
 | 
			
		||||
    ]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -66,6 +66,8 @@ def render("relationships.json", %{user: user, targets: targets}) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  defp do_render("account.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)
 | 
			
		||||
| 
						 | 
				
			
			@ -96,7 +98,7 @@ defp do_render("account.json", %{user: user} = opts) do
 | 
			
		|||
      id: to_string(user.id),
 | 
			
		||||
      username: username_from_nickname(user.nickname),
 | 
			
		||||
      acct: user.nickname,
 | 
			
		||||
      display_name: user.name || user.nickname,
 | 
			
		||||
      display_name: display_name,
 | 
			
		||||
      locked: user_info.locked,
 | 
			
		||||
      created_at: Utils.to_masto_date(user.inserted_at),
 | 
			
		||||
      followers_count: user_info.follower_count,
 | 
			
		||||
| 
						 | 
				
			
			@ -124,12 +126,16 @@ defp do_render("account.json", %{user: user} = opts) do
 | 
			
		|||
        hide_followers: user.info.hide_followers,
 | 
			
		||||
        hide_follows: user.info.hide_follows,
 | 
			
		||||
        hide_favorites: user.info.hide_favorites,
 | 
			
		||||
        relationship: relationship
 | 
			
		||||
        relationship: relationship,
 | 
			
		||||
        skip_thread_containment: user.info.skip_thread_containment,
 | 
			
		||||
        background_image: image_url(user.info.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)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp username_from_nickname(string) when is_binary(string) do
 | 
			
		||||
| 
						 | 
				
			
			@ -152,6 +158,24 @@ defp maybe_put_settings(
 | 
			
		|||
 | 
			
		||||
  defp maybe_put_settings(data, _, _, _), do: data
 | 
			
		||||
 | 
			
		||||
  defp maybe_put_settings_store(data, %User{info: info, id: id}, %User{id: id}, %{
 | 
			
		||||
         with_pleroma_settings: true
 | 
			
		||||
       }) do
 | 
			
		||||
    data
 | 
			
		||||
    |> Kernel.put_in([:pleroma, :settings_store], info.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
 | 
			
		||||
    data
 | 
			
		||||
    |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
 | 
			
		||||
| 
						 | 
				
			
			@ -171,4 +195,7 @@ defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id:
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_put_notification_settings(data, _, _), do: data
 | 
			
		||||
 | 
			
		||||
  defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
 | 
			
		||||
  defp image_url(_), do: nil
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,9 +22,14 @@ def render("participation.json", %{participation: participation, user: user}) do
 | 
			
		|||
 | 
			
		||||
    last_status = StatusView.render("status.json", %{activity: activity, for: user})
 | 
			
		||||
 | 
			
		||||
    # Conversations return all users except the current user.
 | 
			
		||||
    users =
 | 
			
		||||
      participation.conversation.users
 | 
			
		||||
      |> Enum.reject(&(&1.id == user.id))
 | 
			
		||||
 | 
			
		||||
    accounts =
 | 
			
		||||
      AccountView.render("accounts.json", %{
 | 
			
		||||
        users: participation.conversation.users,
 | 
			
		||||
        users: users,
 | 
			
		||||
        as: :user
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -240,6 +240,7 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity
 | 
			
		|||
      spoiler_text: summary_html,
 | 
			
		||||
      visibility: get_visibility(object),
 | 
			
		||||
      media_attachments: attachments,
 | 
			
		||||
      poll: render("poll.json", %{object: object, for: opts[:for]}),
 | 
			
		||||
      mentions: mentions,
 | 
			
		||||
      tags: build_tags(tags),
 | 
			
		||||
      application: %{
 | 
			
		||||
| 
						 | 
				
			
			@ -290,8 +291,8 @@ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
 | 
			
		|||
      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],
 | 
			
		||||
      title: rich_media[:title] || "",
 | 
			
		||||
      description: rich_media[:description] || "",
 | 
			
		||||
      pleroma: %{
 | 
			
		||||
        opengraph: rich_media
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -329,6 +330,64 @@ def render("attachment.json", %{attachment: attachment}) do
 | 
			
		|||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def render("poll.json", %{object: object} = opts) do
 | 
			
		||||
    {multiple, options} =
 | 
			
		||||
      case object.data do
 | 
			
		||||
        %{"anyOf" => options} when is_list(options) -> {true, options}
 | 
			
		||||
        %{"oneOf" => options} when is_list(options) -> {false, options}
 | 
			
		||||
        _ -> {nil, nil}
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    if options do
 | 
			
		||||
      end_time =
 | 
			
		||||
        (object.data["closed"] || object.data["endTime"])
 | 
			
		||||
        |> NaiveDateTime.from_iso8601!()
 | 
			
		||||
 | 
			
		||||
      expired =
 | 
			
		||||
        end_time
 | 
			
		||||
        |> NaiveDateTime.compare(NaiveDateTime.utc_now())
 | 
			
		||||
        |> case do
 | 
			
		||||
          :lt -> true
 | 
			
		||||
          _ -> false
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      voted =
 | 
			
		||||
        if opts[:for] do
 | 
			
		||||
          existing_votes =
 | 
			
		||||
            Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
 | 
			
		||||
 | 
			
		||||
          existing_votes != [] or opts[:for].ap_id == object.data["actor"]
 | 
			
		||||
        else
 | 
			
		||||
          false
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      {options, votes_count} =
 | 
			
		||||
        Enum.map_reduce(options, 0, fn %{"name" => name} = option, count ->
 | 
			
		||||
          current_count = option["replies"]["totalItems"] || 0
 | 
			
		||||
 | 
			
		||||
          {%{
 | 
			
		||||
             title: HTML.strip_tags(name),
 | 
			
		||||
             votes_count: current_count
 | 
			
		||||
           }, current_count + count}
 | 
			
		||||
        end)
 | 
			
		||||
 | 
			
		||||
      %{
 | 
			
		||||
        # Mastodon uses separate ids for polls, but an object can't have
 | 
			
		||||
        # more than one poll embedded so object id is fine
 | 
			
		||||
        id: object.id,
 | 
			
		||||
        expires_at: Utils.to_masto_date(end_time),
 | 
			
		||||
        expired: expired,
 | 
			
		||||
        multiple: multiple,
 | 
			
		||||
        votes_count: votes_count,
 | 
			
		||||
        options: options,
 | 
			
		||||
        voted: voted,
 | 
			
		||||
        emojis: build_emojis(object.data["emoji"])
 | 
			
		||||
      }
 | 
			
		||||
    else
 | 
			
		||||
      nil
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
 | 
			
		||||
    object = Object.normalize(activity)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
 | 
			
		|||
    "public:media",
 | 
			
		||||
    "public:local:media",
 | 
			
		||||
    "user",
 | 
			
		||||
    "user:notification",
 | 
			
		||||
    "direct",
 | 
			
		||||
    "list",
 | 
			
		||||
    "hashtag"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
		Reference in a new issue