Merge branch 'develop' into update-oauth-template

This commit is contained in:
Mark Felder 2019-06-03 09:12:17 -05:00
commit f4e2595592
290 changed files with 10222 additions and 2526 deletions

1
.buildpacks Normal file
View file

@ -0,0 +1 @@
https://github.com/hashnuke/heroku-buildpack-elixir

5
.gitignore vendored
View file

@ -10,6 +10,7 @@
/test/tmp/ /test/tmp/
/doc /doc
/instance /instance
/priv/ssh_keys
# Prevent committing custom emojis # Prevent committing custom emojis
/priv/static/emoji/custom/* /priv/static/emoji/custom/*
@ -37,3 +38,7 @@ erl_crash.dump
# Prevent committing docs files # Prevent committing docs files
/priv/static/doc/* /priv/static/doc/*
# Code test coverage
/cover
/Elixir.*.coverdata

View file

@ -45,11 +45,28 @@ docs-build:
unit-testing: unit-testing:
stage: test stage: test
services: services:
- name: postgres:9.6.2 - name: lainsoykaf/postgres-with-rum
alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
script: script:
- mix deps.get
- mix ecto.create - mix ecto.create
- mix ecto.migrate - mix ecto.migrate
- mix coveralls --trace --preload-modules
unit-testing-rum:
stage: test
services:
- name: lainsoykaf/postgres-with-rum
alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
variables:
RUM_ENABLED: "true"
script:
- mix deps.get
- mix ecto.create
- mix ecto.migrate
- "mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/"
- mix test --trace --preload-modules - mix test --trace --preload-modules
lint: lint:
@ -63,7 +80,6 @@ analysis:
- mix deps.get - mix deps.get
- mix credo --strict --only=warnings,todo,fixme,consistency,readability - mix credo --strict --only=warnings,todo,fixme,consistency,readability
docs-deploy: docs-deploy:
stage: deploy stage: deploy
image: alpine:3.9 image: alpine:3.9
@ -77,4 +93,50 @@ docs-deploy:
- echo "${SSH_HOST_KEY}" > ~/.ssh/known_hosts - echo "${SSH_HOST_KEY}" > ~/.ssh/known_hosts
- eval $(ssh-agent -s) - eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - 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}" - 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

View file

@ -5,29 +5,49 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [unreleased] ## [unreleased]
### Added ### Added
- Optional SSH access mode. (Needs `erlang-ssh` package on some distributions).
- [MongooseIM](https://github.com/esl/MongooseIM) http authentication support.
- LDAP authentication - LDAP authentication
- External OAuth provider authentication - External OAuth provider authentication
- A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc. - A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc.
- [Prometheus](https://prometheus.io/) metrics - [Prometheus](https://prometheus.io/) metrics
- Support for Mastodon's remote interaction - Support for Mastodon's remote interaction
- Mix Tasks: `mix pleroma.database bump_all_conversations`
- Mix Tasks: `mix pleroma.database remove_embedded_objects` - Mix Tasks: `mix pleroma.database remove_embedded_objects`
- Mix Tasks: `mix pleroma.database update_users_following_followers_counts`
- Mix Tasks: `mix pleroma.user toggle_confirmed`
- Federation: Support for `Question` and `Answer` objects
- Federation: Support for reports - Federation: Support for reports
- Configuration: `poll_limits` option
- Configuration: `safe_dm_mentions` option - Configuration: `safe_dm_mentions` option
- Configuration: `link_name` option - Configuration: `link_name` option
- Configuration: `fetch_initial_posts` option - Configuration: `fetch_initial_posts` option
- Configuration: `notify_email` option - Configuration: `notify_email` option
- Configuration: Media proxy `whitelist` option - Configuration: Media proxy `whitelist` option
- Configuration: `report_uri` option
- Pleroma API: User subscriptions - Pleroma API: User subscriptions
- Pleroma API: Healthcheck endpoint - Pleroma API: Healthcheck endpoint
- Pleroma API: `/api/v1/pleroma/mascot` per-user frontend mascot configuration endpoints
- Admin API: Endpoints for listing/revoking invite tokens - Admin API: Endpoints for listing/revoking invite tokens
- Admin API: Endpoints for making users follow/unfollow each other - Admin API: Endpoints for making users follow/unfollow each other
- Admin API: added filters (role, tags, email, name) for users endpoint
- Admin API: Endpoints for managing reports
- Admin API: Endpoints for deleting and changing the scope of individual reported statuses
- AdminFE: initial release with basic user management accessible at /pleroma/admin/
- Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/) - 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/notifications/destroy_multiple` (glitch-soc extension)
- Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension) - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
- Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/) - 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 - ActivityPub C2S: OAuth endpoints
- Metadata RelMe provider - Metadata: RelMe provider
- OAuth: added support for refresh tokens
- Emoji packs and emoji pack manager - Emoji packs and emoji pack manager
- Object pruning (`mix pleroma.database prune_objects`)
- OAuth: added job to clean expired access tokens
- MRF: Support for rejecting reports from specific instances (`mrf_simple`)
- MRF: Support for stripping avatars and banner images from specific instances (`mrf_simple`)
### Changed ### Changed
- **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer - **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
@ -40,8 +60,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Federation: Removed `inReplyToStatusId` from objects - Federation: Removed `inReplyToStatusId` from objects
- Configuration: Dedupe enabled by default - Configuration: Dedupe enabled by default
- Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work. - Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work.
- Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change
- Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats. - 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`
- Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change
- Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications` - Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications`
- Mastodon API: Add `languages` and `registrations` to `/api/v1/instance` - Mastodon API: Add `languages` and `registrations` to `/api/v1/instance`
- Mastodon API: Provide plaintext versions of cw/content in the Status entity - Mastodon API: Provide plaintext versions of cw/content in the Status entity
@ -55,16 +76,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Add `with_muted` parameter to timeline endpoints - Mastodon API: Add `with_muted` parameter to timeline endpoints
- Mastodon API: Actual reblog hiding instead of a dummy - Mastodon API: Actual reblog hiding instead of a dummy
- Mastodon API: Remove attachment limit in the Status entity - Mastodon API: Remove attachment limit in the Status entity
- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints.
- Deps: Updated Cowboy to 2.6 - Deps: Updated Cowboy to 2.6
- Deps: Updated Ecto to 3.0.7 - Deps: Updated Ecto to 3.0.7
- Don't ship finmoji by default, they can be installed as an emoji pack - Don't ship finmoji by default, they can be installed as an emoji pack
- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints. - Hide deactivated users and their statuses
- Posts which are marked sensitive or tagged nsfw no longer have link previews.
- HTTP connection timeout is now set to 10 seconds.
- Respond with a 404 Not implemented JSON error message when requested API is not implemented
### Fixed ### Fixed
- 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 - Followers counter not being updated when a follower is blocked
- Deactivated users being able to request an access token - Deactivated users being able to request an access token
- Limit on request body in rich media/relme parsers being ignored resulting in a possible memory leak - Limit on request body in rich media/relme parsers being ignored resulting in a possible memory leak
- proper Twitter Card generation instead of a dummy - Proper Twitter Card generation instead of a dummy
- Deletions failing for users with a large number of posts
- NodeInfo: Include admins in `staffAccounts` - NodeInfo: Include admins in `staffAccounts`
- ActivityPub: Crashing when requesting empty local user's outbox - ActivityPub: Crashing when requesting empty local user's outbox
- Federation: Handling of objects without `summary` property - Federation: Handling of objects without `summary` property
@ -87,6 +114,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Handling of `reblogs` in `/api/v1/accounts/:id/follow` - Mastodon API: Handling of `reblogs` in `/api/v1/accounts/:id/follow`
- Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON - Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON
- Mastodon API: Exposing default scope of the user to anyone - 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 ## [0.9.9999] - 2019-04-05
### Security ### Security

View file

@ -15,6 +15,14 @@ priv/static/images/pleroma-tan.png
--- ---
The following files are copyright © 2019 shitposter.club, and are distributed
under the Creative Commons Attribution 4.0 International license, you should
have received a copy of the license file as CC-BY-4.0.
priv/static/images/pleroma-fox-tan-shy.png
---
The following files are copyright © 2017-2019 Pleroma Authors The following files are copyright © 2017-2019 Pleroma Authors
<https://pleroma.social/>, and are distributed under the Creative Commons <https://pleroma.social/>, and are distributed under the Creative Commons
Attribution-ShareAlike 4.0 International license, you should have received Attribution-ShareAlike 4.0 International license, you should have received

2
Procfile Normal file
View file

@ -0,0 +1,2 @@
web: mix phx.server
release: mix ecto.migrate

View file

@ -12,7 +12,7 @@ For clients it supports both the [GNU Social API with Qvitter extensions](https:
- [Client Applications for Pleroma](https://docs-develop.pleroma.social/clients.html) - [Client Applications for Pleroma](https://docs-develop.pleroma.social/clients.html)
No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>.
## Installation ## Installation

View file

@ -48,7 +48,8 @@
config :pleroma, Pleroma.Repo, config :pleroma, Pleroma.Repo,
types: Pleroma.PostgresTypes, types: Pleroma.PostgresTypes,
telemetry_event: [Pleroma.Repo.Instrumenter] telemetry_event: [Pleroma.Repo.Instrumenter],
migration_lock: nil
config :pleroma, Pleroma.Captcha, config :pleroma, Pleroma.Captcha,
enabled: false, enabled: false,
@ -183,14 +184,12 @@
"application/ld+json" => ["activity+json"] "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 config :tesla, adapter: Tesla.Adapter.Hackney
# Configures http settings, upstream proxy etc. # Configures http settings, upstream proxy etc.
config :pleroma, :http, config :pleroma, :http,
proxy_url: nil, proxy_url: nil,
send_user_agent: true,
adapter: [ adapter: [
ssl_options: [ ssl_options: [
# We don't support TLS v1.3 yet # We don't support TLS v1.3 yet
@ -209,9 +208,20 @@
avatar_upload_limit: 2_000_000, avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000, background_upload_limit: 4_000_000,
banner_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, registrations_open: true,
federating: true, federating: true,
federation_reachability_timeout_days: 7, federation_reachability_timeout_days: 7,
federation_publisher_modules: [
Pleroma.Web.ActivityPub.Publisher,
Pleroma.Web.Websub,
Pleroma.Web.Salmon
],
allow_relay: true, allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true, public: true,
@ -232,7 +242,10 @@
welcome_message: nil, welcome_message: nil,
max_report_comment_size: 1000, max_report_comment_size: 1000,
safe_dm_mentions: false, safe_dm_mentions: false,
healthcheck: false healthcheck: false,
remote_post_retention_days: 90
config :pleroma, :app_account_creation, enabled: true, max_requests: 25, interval: 1800
config :pleroma, :markup, config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because # XXX - unfortunately, inline images must be enabled by default right now, because
@ -246,25 +259,6 @@
Pleroma.HTML.Scrubber.Default Pleroma.HTML.Scrubber.Default
] ]
# Deprecated, will be gone in 1.0
config :pleroma, :fe,
theme: "pleroma-dark",
logo: "/static/logo.png",
logo_mask: true,
logo_margin: "0.1em",
background: "/static/aurora_borealis.jpg",
redirect_root_no_login: "/main/all",
redirect_root_login: "/main/friends",
show_instance_panel: true,
scope_options_enabled: false,
formatting_options_enabled: false,
collapse_message_with_subject: false,
hide_post_stats: false,
hide_user_stats: false,
scope_copy: true,
subject_line_behavior: "email",
always_show_subject_input: true
config :pleroma, :frontend_configurations, config :pleroma, :frontend_configurations,
pleroma_fe: %{ pleroma_fe: %{
theme: "pleroma-dark", theme: "pleroma-dark",
@ -286,6 +280,19 @@
showInstanceSpecificPanel: true showInstanceSpecificPanel: true
} }
config :pleroma, :assets,
mascots: [
pleroma_fox_tan: %{
url: "/images/pleroma-fox-tan-smol.png",
mime_type: "image/png"
},
pleroma_fox_tan_shy: %{
url: "/images/pleroma-fox-tan-shy.png",
mime_type: "image/png"
}
],
default_mascot: :pleroma_fox_tan
config :pleroma, :activitypub, config :pleroma, :activitypub,
accept_blocks: true, accept_blocks: true,
unfollow_blocked: true, unfollow_blocked: true,
@ -308,8 +315,11 @@
media_removal: [], media_removal: [],
media_nsfw: [], media_nsfw: [],
federated_timeline_removal: [], federated_timeline_removal: [],
report_removal: [],
reject: [], reject: [],
accept: [] accept: [],
avatar_removal: [],
banner_removal: []
config :pleroma, :mrf_keyword, config :pleroma, :mrf_keyword,
reject: [], reject: [],
@ -380,6 +390,7 @@
"activities", "activities",
"api", "api",
"auth", "auth",
"check_password",
"dev", "dev",
"friend-requests", "friend-requests",
"inbox", "inbox",
@ -400,6 +411,7 @@
"status", "status",
"tag", "tag",
"user-search", "user-search",
"user_exists",
"users", "users",
"web" "web"
] ]
@ -416,7 +428,8 @@
web_push: 50, web_push: 50,
mailer: 10, mailer: 10,
transmogrifier: 20, transmogrifier: 20,
scheduled_activities: 10 scheduled_activities: 10,
background: 5
config :pleroma, :fetch_initial_posts, config :pleroma, :fetch_initial_posts,
enabled: false, enabled: false,
@ -443,6 +456,9 @@
base: System.get_env("LDAP_BASE") || "dc=example,dc=com", base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
uid: System.get_env("LDAP_UID") || "cn" uid: System.get_env("LDAP_UID") || "cn"
config :esshd,
enabled: false
oauth_consumer_strategies = String.split(System.get_env("OAUTH_CONSUMER_STRATEGIES") || "") oauth_consumer_strategies = String.split(System.get_env("OAUTH_CONSUMER_STRATEGIES") || "")
ueberauth_providers = ueberauth_providers =
@ -468,6 +484,17 @@
total_user_limit: 300, total_user_limit: 300,
enabled: true enabled: true
config :pleroma, :oauth2,
token_expires_in: 600,
issue_new_refresh_token: true,
clean_expired_tokens: false,
clean_expired_tokens_interval: 86_400_000
config :pleroma, :database, rum_enabled: false
config :http_signatures,
adapter: Pleroma.Signature
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

25
config/dokku.exs Normal file
View 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"

View file

@ -39,8 +39,6 @@
# Reduce hash rounds for testing # Reduce hash rounds for testing
config :pbkdf2_elixir, rounds: 1 config :pbkdf2_elixir, rounds: 1
config :pleroma, :websub, Pleroma.Web.WebsubMock
config :pleroma, :ostatus, Pleroma.Web.OStatusMock
config :tesla, adapter: Tesla.Mock config :tesla, adapter: Tesla.Mock
config :pleroma, :rich_media, enabled: false config :pleroma, :rich_media, enabled: false
@ -59,6 +57,16 @@
total_user_limit: 3, total_user_limit: 3,
enabled: false enabled: false
config :pleroma, :app_account_creation, max_requests: 5
config :pleroma, :http_security, report_uri: "https://endpoint.com"
config :pleroma, :http, send_user_agent: false
rum_enabled = System.get_env("RUM_ENABLED") == "true"
config :pleroma, :database, rum_enabled: rum_enabled
IO.puts("RUM enabled: #{rum_enabled}")
try do try do
import_config "test.secret.exs" import_config "test.secret.exs"
rescue rescue

View file

@ -8,18 +8,23 @@ Authentication is required and the user must be an admin.
- Method `GET` - Method `GET`
- Query Params: - Query Params:
- *optional* `query`: **string** search term - *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain)
- *optional* `filters`: **string** comma-separated string of filters: - *optional* `filters`: **string** comma-separated string of filters:
- `local`: only local users - `local`: only local users
- `external`: only external users - `external`: only external users
- `active`: only active users - `active`: only active users
- `deactivated`: only deactivated users - `deactivated`: only deactivated users
- `is_admin`: users with admin role
- `is_moderator`: users with moderator role
- *optional* `page`: **integer** page number - *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of users per page (default is `50`) - *optional* `page_size`: **integer** number of users per page (default is `50`)
- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10` - *optional* `tags`: **[string]** tags list
- *optional* `name`: **string** user display name
- *optional* `email`: **string** user email
- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com`
- Response: - Response:
```JSON ```json
{ {
"page_size": integer, "page_size": integer,
"count": integer, "count": integer,
@ -40,7 +45,7 @@ Authentication is required and the user must be an admin.
} }
``` ```
## `/api/pleroma/admin/user` ## `/api/pleroma/admin/users`
### Remove a user ### Remove a user
@ -58,7 +63,7 @@ Authentication is required and the user must be an admin.
- `password` - `password`
- Response: Users nickname - Response: Users nickname
## `/api/pleroma/admin/user/follow` ## `/api/pleroma/admin/users/follow`
### Make a user follow another user ### Make a user follow another user
- Methods: `POST` - Methods: `POST`
@ -68,7 +73,7 @@ Authentication is required and the user must be an admin.
- Response: - Response:
- "ok" - "ok"
## `/api/pleroma/admin/user/unfollow` ## `/api/pleroma/admin/users/unfollow`
### Make a user unfollow another user ### Make a user unfollow another user
- Methods: `POST` - Methods: `POST`
@ -87,7 +92,7 @@ Authentication is required and the user must be an admin.
- `nickname` - `nickname`
- Response: Users object - Response: Users object
```JSON ```json
{ {
"deactivated": bool, "deactivated": bool,
"id": integer, "id": integer,
@ -101,17 +106,17 @@ Authentication is required and the user must be an admin.
- Method: `PUT` - Method: `PUT`
- Params: - Params:
- `nickname` - `nicknames` (array)
- `tags` - `tags` (array)
### Untag a list of users ### Untag a list of users
- Method: `DELETE` - Method: `DELETE`
- Params: - Params:
- `nickname` - `nicknames` (array)
- `tags` - `tags` (array)
## `/api/pleroma/admin/permission_group/:nickname` ## `/api/pleroma/admin/users/:nickname/permission_group`
### Get user user permission groups membership ### Get user user permission groups membership
@ -119,14 +124,14 @@ Authentication is required and the user must be an admin.
- Params: none - Params: none
- Response: - Response:
```JSON ```json
{ {
"is_moderator": bool, "is_moderator": bool,
"is_admin": bool "is_admin": bool
} }
``` ```
## `/api/pleroma/admin/permission_group/:nickname/:permission_group` ## `/api/pleroma/admin/users/:nickname/permission_group/:permission_group`
Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesnt exist. Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesnt exist.
@ -136,7 +141,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Params: none - Params: none
- Response: - Response:
```JSON ```json
{ {
"is_moderator": bool, "is_moderator": bool,
"is_admin": bool "is_admin": bool
@ -160,7 +165,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- On success: JSON of the `user.info` - On success: JSON of the `user.info`
- Note: An admin cannot revoke their own admin status. - Note: An admin cannot revoke their own admin status.
## `/api/pleroma/admin/activation_status/:nickname` ## `/api/pleroma/admin/users/:nickname/activation_status`
### Active or deactivate a user ### Active or deactivate a user
@ -198,7 +203,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Response: - Response:
- On success: URL of the unfollowed relay - On success: URL of the unfollowed relay
## `/api/pleroma/admin/invite_token` ## `/api/pleroma/admin/users/invite_token`
### Get an account registration invite token ### Get an account registration invite token
@ -210,7 +215,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
] ]
- Response: invite token (base64 string) - Response: invite token (base64 string)
## `/api/pleroma/admin/invites` ## `/api/pleroma/admin/users/invites`
### Get a list of generated invites ### Get a list of generated invites
@ -218,7 +223,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Params: none - Params: none
- Response: - Response:
```JSON ```json
{ {
"invites": [ "invites": [
@ -236,7 +241,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## `/api/pleroma/admin/revoke_invite` ## `/api/pleroma/admin/users/revoke_invite`
### Revoke invite by token ### Revoke invite by token
@ -245,7 +250,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `token` - `token`
- Response: - Response:
```JSON ```json
{ {
"id": integer, "id": integer,
"token": string, "token": string,
@ -259,7 +264,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
``` ```
## `/api/pleroma/admin/email_invite` ## `/api/pleroma/admin/users/email_invite`
### Sends registration invite via email ### Sends registration invite via email
@ -268,10 +273,287 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `email` - `email`
- `name`, optional - `name`, optional
## `/api/pleroma/admin/password_reset` ## `/api/pleroma/admin/users/:nickname/password_reset`
### Get a password reset token for a given nickname ### Get a password reset token for a given nickname
- Methods: `GET` - Methods: `GET`
- Params: none - Params: none
- Response: password reset token (base64 string) - Response: password reset token (base64 string)
## `/api/pleroma/admin/reports`
### Get a list of reports
- Method `GET`
- Params:
- `state`: optional, the state of reports. Valid values are `open`, `closed` and `resolved`
- `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:
- On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin
- On success: JSON, returns a list of reports, where:
- `account`: the user who has been reported
- `actor`: the user who has sent the report
- `statuses`: list of statuses that have been included to the report
```json
{
"reports": [
{
"account": {
"acct": "user",
"avatar": "https://pleroma.example.org/images/avi.png",
"avatar_static": "https://pleroma.example.org/images/avi.png",
"bot": false,
"created_at": "2019-04-23T17:32:04.000Z",
"display_name": "User",
"emojis": [],
"fields": [],
"followers_count": 1,
"following_count": 1,
"header": "https://pleroma.example.org/images/banner.png",
"header_static": "https://pleroma.example.org/images/banner.png",
"id": "9i6dAJqSGSKMzLG2Lo",
"locked": false,
"note": "",
"pleroma": {
"confirmation_pending": false,
"hide_favorites": true,
"hide_followers": false,
"hide_follows": false,
"is_admin": false,
"is_moderator": false,
"relationship": {},
"tags": []
},
"source": {
"note": "",
"pleroma": {},
"sensitive": false
},
"statuses_count": 3,
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
"actor": {
"acct": "lain",
"avatar": "https://pleroma.example.org/images/avi.png",
"avatar_static": "https://pleroma.example.org/images/avi.png",
"bot": false,
"created_at": "2019-03-28T17:36:03.000Z",
"display_name": "Roger Braun",
"emojis": [],
"fields": [],
"followers_count": 1,
"following_count": 1,
"header": "https://pleroma.example.org/images/banner.png",
"header_static": "https://pleroma.example.org/images/banner.png",
"id": "9hEkA5JsvAdlSrocam",
"locked": false,
"note": "",
"pleroma": {
"confirmation_pending": false,
"hide_favorites": false,
"hide_followers": false,
"hide_follows": false,
"is_admin": false,
"is_moderator": false,
"relationship": {},
"tags": []
},
"source": {
"note": "",
"pleroma": {},
"sensitive": false
},
"statuses_count": 1,
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
"content": "Please delete it",
"created_at": "2019-04-29T19:48:15.000Z",
"id": "9iJGOv1j8hxuw19bcm",
"state": "open",
"statuses": [
{
"account": { ... },
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<span class=\"h-card\"><a data-user=\"9hEkA5JsvAdlSrocam\" class=\"u-url mention\" href=\"https://pleroma.example.org/users/lain\">@<span>lain</span></a></span> click on my link <a href=\"https://www.google.com/\">https://www.google.com/</a>",
"created_at": "2019-04-23T19:15:47.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9i6mQ9uVrrOmOime8m",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "lain",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "@lain click on my link https://www.google.com/"
},
"conversation_id": 28,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/8717b90f-8e09-4b58-97b0-e3305472b396",
"url": "https://pleroma.example.org/notice/9i6mQ9uVrrOmOime8m",
"visibility": "direct"
}
]
}
]
}
```
## `/api/pleroma/admin/reports/:id`
### Get an individual report
- Method `GET`
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id`
### Change the state of the report
- Method `PUT`
- Params:
- `id`
- `state`: required, the new state. Valid values are `open`, `closed` and `resolved`
- Response:
- On failure:
- 400 Bad Request `"Unsupported state"`
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id/respond`
### Respond to a report
- Method `POST`
- Params:
- `id`
- `status`: required, the message
- Response:
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, created Mastodon Status entity
```json
{
"account": { ... },
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "Your claim is going to be closed",
"created_at": "2019-05-11T17:13:03.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9ihuiSL1405I65TmEq",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
{
"acct": "admin",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/admin",
"username": "admin"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "Your claim is going to be closed"
},
"conversation_id": 35,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/cab0836d-9814-46cd-a0ea-529da9db5fcb",
"url": "https://pleroma.example.org/notice/9ihuiSL1405I65TmEq",
"visibility": "direct"
}
```
## `/api/pleroma/admin/statuses/:id`
### Change the scope of an individual reported status
- Method `PUT`
- Params:
- `id`
- `sensitive`: optional, valid values are `true` or `false`
- `visibility`: optional, valid values are `public`, `private` and `unlisted`
- Response:
- On failure:
- 400 Bad Request `"Unsupported visibility"`
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Mastodon Status entity
## `/api/pleroma/admin/statuses/:id`
### Delete an individual reported status
- Method `DELETE`
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: 200 OK `{}`

View file

@ -1,6 +1,6 @@
# Differences in Mastodon API responses from vanilla Mastodon # Differences in Mastodon API responses from vanilla Mastodon
A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma <version>)" present in `version` field in response from `/api/v1/instance` A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma <version>)" present in `version` field in response from `/api/v1/instance`
## Flake IDs ## Flake IDs
@ -80,3 +80,20 @@ Additional parameters can be added to the JSON body/Form data:
- `hide_favorites` - if true, user's favorites timeline will be hidden - `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 - `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 - `default_scope` - the scope returned under `privacy` key in Source subentity
## Authentication
*Pleroma supports refreshing tokens.
`POST /oauth/token`
Post here request with grant_type=refresh_token to obtain new access token. Returns an access token.
## Account Registration
`POST /api/v1/accounts`
Has theses additionnal parameters (which are the same as in Pleroma-API):
* `fullname`: optional
* `bio`: optional
* `captcha_solution`: optional, contains provider-specific captcha solution,
* `captcha_token`: optional, contains provider-specific captcha token
* `token`: invite token required when the registerations aren't public.

View file

@ -61,6 +61,15 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
* Response: JSON. Returns `{"status": "success"}` if the deletion was successful, `{"error": "[error message]"}` otherwise * Response: JSON. Returns `{"status": "success"}` if the deletion was successful, `{"error": "[error message]"}` otherwise
* Example response: `{"error": "Invalid password."}` * Example response: `{"error": "Invalid password."}`
## `/api/pleroma/disable_account`
### Disable an account
* Method `POST`
* Authentication: required
* Params:
* `password`: user's password
* Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise
* Example response: `{"error": "Invalid password."}`
## `/api/account/register` ## `/api/account/register`
### Register a new user ### Register a new user
* Method `POST` * Method `POST`
@ -117,20 +126,6 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
## `/api/pleroma/admin/` ## `/api/pleroma/admin/`
See [Admin-API](Admin-API.md) 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` ## `/api/pleroma/notifications/read`
### Mark a single notification as read ### Mark a single notification as read
* Method `POST` * Method `POST`
@ -243,6 +238,45 @@ See [Admin-API](Admin-API.md)
] ]
``` ```
## `/api/v1/pleroma/mascot`
### Gets user mascot image
* Method `GET`
* Authentication: required
* Response: JSON. Returns a mastodon media attachment entity.
* Example response:
```json
{
"id": "abcdefg",
"url": "https://pleroma.example.org/media/abcdefg.png",
"type": "image",
"pleroma": {
"mime_type": "image/png"
}
}
```
### Updates user mascot image
* Method `PUT`
* Authentication: required
* Params:
* `image`: Multipart image
* Response: JSON. Returns a mastodon media attachment entity
when successful, otherwise returns HTTP 415 `{"error": "error_msg"}`
* Example response:
```json
{
"id": "abcdefg",
"url": "https://pleroma.example.org/media/abcdefg.png",
"type": "image",
"pleroma": {
"mime_type": "image/png"
}
}
```
* Note: Behaves exactly the same as `POST /api/v1/upload`.
Can only accept images - any attempt to upload non-image files will be met with `HTTP 415 Unsupported Media Type`.
## `/api/pleroma/notification_settings` ## `/api/pleroma/notification_settings`
### Updates user notification settings ### Updates user notification settings
* Method `PUT` * Method `PUT`

View file

@ -37,7 +37,7 @@ This filter replaces the filename (not the path) of an upload. For complete obfu
An example for Sendgrid adapter: An example for Sendgrid adapter:
```exs ```elixir
config :pleroma, Pleroma.Emails.Mailer, config :pleroma, Pleroma.Emails.Mailer,
adapter: Swoosh.Adapters.Sendgrid, adapter: Swoosh.Adapters.Sendgrid,
api_key: "YOUR_API_KEY" api_key: "YOUR_API_KEY"
@ -45,7 +45,7 @@ config :pleroma, Pleroma.Emails.Mailer,
An example for SMTP adapter: An example for SMTP adapter:
```exs ```elixir
config :pleroma, Pleroma.Emails.Mailer, config :pleroma, Pleroma.Emails.Mailer,
adapter: Swoosh.Adapters.SMTP, adapter: Swoosh.Adapters.SMTP,
relay: "smtp.gmail.com", relay: "smtp.gmail.com",
@ -71,6 +71,11 @@ config :pleroma, Pleroma.Emails.Mailer,
* `avatar_upload_limit`: File size limit of users profile avatars * `avatar_upload_limit`: File size limit of users profile avatars
* `background_upload_limit`: File size limit of users profile backgrounds * `background_upload_limit`: File size limit of users profile backgrounds
* `banner_upload_limit`: File size limit of users profile banners * `banner_upload_limit`: File size limit of users 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. * `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
* `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: 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. * `account_activation_required`: Require users to confirm their emails before signing in.
@ -104,12 +109,19 @@ config :pleroma, Pleroma.Emails.Mailer,
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`) * `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`) * `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``. * `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
## :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 ## :logger
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack
An example to enable ONLY ExSyslogger (f/ex in ``prod.secret.exs``) with info and debug suppressed: An example to enable ONLY ExSyslogger (f/ex in ``prod.secret.exs``) with info and debug suppressed:
``` ```elixir
config :logger, config :logger,
backends: [{ExSyslogger, :ex_syslogger}] backends: [{ExSyslogger, :ex_syslogger}]
@ -118,7 +130,7 @@ config :logger, :ex_syslogger,
``` ```
Another example, keeping console output and adding the pid to syslog output: Another example, keeping console output and adding the pid to syslog output:
``` ```elixir
config :logger, config :logger,
backends: [:console, {ExSyslogger, :ex_syslogger}] backends: [:console, {ExSyslogger, :ex_syslogger}]
@ -130,7 +142,7 @@ config :logger, :ex_syslogger,
See: [loggers documentation](https://hexdocs.pm/logger/Logger.html) and [ex_sysloggers documentation](https://hexdocs.pm/ex_syslogger/) See: [loggers documentation](https://hexdocs.pm/logger/Logger.html) and [ex_sysloggers documentation](https://hexdocs.pm/ex_syslogger/)
An example of logging info to local syslog, but warn to a Slack channel: An example of logging info to local syslog, but warn to a Slack channel:
``` ```elixir
config :logger, config :logger,
backends: [ {ExSyslogger, :ex_syslogger}, Quack.Logger ], backends: [ {ExSyslogger, :ex_syslogger}, Quack.Logger ],
level: :info level: :info
@ -156,14 +168,30 @@ Frontends can access these settings at `/api/pleroma/frontend_configurations`
To add your own configuration for PleromaFE, use it like this: To add your own configuration for PleromaFE, use it like this:
`config :pleroma, :frontend_configurations, pleroma_fe: %{redirectRootNoLogin: "/main/all", ...}` ```elixir
config :pleroma, :frontend_configurations,
pleroma_fe: %{
theme: "pleroma-dark",
# ... see /priv/static/static/config.json for the available keys.
},
masto_fe: %{
showInstanceSpecificPanel: true
}
```
These settings need to be complete, they will override the defaults. See `priv/static/static/config.json` for the available keys. These settings **need to be complete**, they will override the defaults.
NOTE: for versions < 1.0, you need to set [`:fe`](#fe) to false, as shown a few lines below.
## :fe ## :fe
__THIS IS DEPRECATED__ __THIS IS DEPRECATED__
If you are using this method, please change it to the `frontend_configurations` method. Please set this option to false in your config like this: `config :pleroma, :fe, false`. If you are using this method, please change it to the [`frontend_configurations`](#frontend_configurations) method.
Please **set this option to false** in your config like this:
```elixir
config :pleroma, :fe, false
```
This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false. This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false.
@ -181,12 +209,25 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
* `hide_post_stats`: Hide notices statistics(repeats, favorites, …) * `hide_post_stats`: Hide notices statistics(repeats, favorites, …)
* `hide_user_stats`: Hide profile statistics(posts, posts per day, followers, followings, …) * `hide_user_stats`: Hide profile statistics(posts, posts per day, followers, followings, …)
## :assets
This section configures assets to be used with various frontends. Currently the only option
relates to mascots on the mastodon frontend
* `mascots`: KeywordList of mascots, each element __MUST__ contain both a `url` and a
`mime_type` key.
* `default_mascot`: An element from `mascots` - This will be used as the default mascot
on MastoFE (default: `:pleroma_fox_tan`)
## :mrf_simple ## :mrf_simple
* `media_removal`: List of instances to remove medias from * `media_removal`: List of instances to remove medias from
* `media_nsfw`: List of instances to put medias as NSFW(sensitive) from * `media_nsfw`: List of instances to put medias as NSFW(sensitive) from
* `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline * `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline
* `reject`: List of instances to reject any activities from * `reject`: List of instances to reject any activities from
* `accept`: List of instances to accept any activities from * `accept`: List of instances to accept any activities from
* `report_removal`: List of instances to reject reports from
* `avatar_removal`: List of instances to strip avatars from
* `banner_removal`: List of instances to strip banners from
## :mrf_rejectnonpublic ## :mrf_rejectnonpublic
* `allow_followersonly`: whether to allow followers-only posts * `allow_followersonly`: whether to allow followers-only posts
@ -264,7 +305,8 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start
* ``sts``: Whether to additionally send a `Strict-Transport-Security` header * ``sts``: Whether to additionally send a `Strict-Transport-Security` header
* ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent * ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent
* ``ct_max_age``: The maximum age for the `Expect-CT` header if sent * ``ct_max_age``: The maximum age for the `Expect-CT` header if sent
* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`. * ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`
* ``report_uri``: Adds the specified url to `report-uri` and `report-to` group in CSP header.
## :mrf_user_allowlist ## :mrf_user_allowlist
@ -274,7 +316,7 @@ their ActivityPub ID.
An example: An example:
```exs ```elixir
config :pleroma, :mrf_user_allowlist, config :pleroma, :mrf_user_allowlist,
"example.org": ["https://example.org/users/admin"] "example.org": ["https://example.org/users/admin"]
``` ```
@ -303,7 +345,7 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end
Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the 'admin_token' parameter. Example: Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the 'admin_token' parameter. Example:
```exs ```elixir
config :pleroma, :admin_token, "somerandomtoken" config :pleroma, :admin_token, "somerandomtoken"
``` ```
@ -387,7 +429,7 @@ Configuration for the `auto_linker` library:
Example: Example:
```exs ```elixir
config :auto_linker, config :auto_linker,
opts: [ opts: [
scheme: true, scheme: true,
@ -428,15 +470,36 @@ Pleroma account will be created with the same name as the LDAP user name.
* `base`: LDAP base, e.g. "dc=example,dc=com" * `base`: LDAP base, e.g. "dc=example,dc=com"
* `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base" * `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base"
## BBS / SSH access
To enable simple command line interface accessible over ssh, add a setting like this to your configuration file:
```exs
app_dir = File.cwd!
priv_dir = Path.join([app_dir, "priv/ssh_keys"])
config :esshd,
enabled: true,
priv_dir: priv_dir,
handler: "Pleroma.BBS.Handler",
port: 10_022,
password_authenticator: "Pleroma.BBS.Authenticator"
```
Feel free to adjust the priv_dir and port number. Then you will have to create the key for the keys (in the example `priv/ssh_keys`) and create the host keys with `ssh-keygen -m PEM -N "" -b 2048 -t rsa -f ssh_host_rsa_key`. After restarting, you should be able to connect to your Pleroma instance with `ssh username@server -p $PORT`
## :auth ## :auth
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication
Authentication / authorization settings. 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`. * `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_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.
# OAuth consumer mode ## OAuth consumer mode
OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.).
Implementation is based on Ueberauth; see the list of [available strategies](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies). Implementation is based on Ueberauth; see the list of [available strategies](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies).
@ -460,7 +523,7 @@ Note: make sure that `"SameSite=Lax"` is set in `extra_cookie_attrs` when you ha
Once the app is configured on external OAuth provider side, add app's credentials and strategy-specific settings (if any — e.g. see Microsoft below) to `config/prod.secret.exs`, Once the app is configured on external OAuth provider side, add app's credentials and strategy-specific settings (if any — e.g. see Microsoft below) to `config/prod.secret.exs`,
per strategy's documentation (e.g. [ueberauth_twitter](https://github.com/ueberauth/ueberauth_twitter)). Example config basing on environment variables: per strategy's documentation (e.g. [ueberauth_twitter](https://github.com/ueberauth/ueberauth_twitter)). Example config basing on environment variables:
``` ```elixir
# Twitter # Twitter
config :ueberauth, Ueberauth.Strategy.Twitter.OAuth, config :ueberauth, Ueberauth.Strategy.Twitter.OAuth,
consumer_key: System.get_env("TWITTER_CONSUMER_KEY"), consumer_key: System.get_env("TWITTER_CONSUMER_KEY"),
@ -489,7 +552,31 @@ config :ueberauth, Ueberauth,
] ]
``` ```
## OAuth 2.0 provider - :oauth2
Configure OAuth 2 provider capabilities:
* `token_expires_in` - The lifetime in seconds of the access token.
* `issue_new_refresh_token` - Keeps old refresh token or generate new refresh token when to obtain an access token.
* `clean_expired_tokens` - Enable a background job to clean expired oauth tokens. Defaults to `false`.
* `clean_expired_tokens_interval` - Interval to run the job to clean expired tokens. Defaults to `86_400_000` (24 hours).
## :emoji ## :emoji
* `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]` * `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]`
* `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]` * `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]`
* `default_manifest`: Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays). * `default_manifest`: Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays).
## Database options
### RUM indexing for full text search
* `rum_enabled`: If RUM indexes should be used. Defaults to `false`.
RUM indexes are an alternative indexing scheme that is not included in PostgreSQL by default. While they may eventually be mainlined, for now they have to be installed as a PostgreSQL extension from https://github.com/postgrespro/rum.
Their advantage over the standard GIN indexes is that they allow efficient ordering of search results by timestamp, which makes search queries a lot faster on larger servers, by one or two orders of magnitude. They take up around 3 times as much space as GIN indexes.
To enable them, both the `rum_enabled` flag has to be set and the following special migration has to be run:
`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`
This will probably take a long time.

View file

@ -0,0 +1,10 @@
# Configuring MongooseIM (XMPP Server) to use Pleroma for authentication
If you want to give your Pleroma users an XMPP (chat) account, you can configure [MongooseIM](https://github.com/esl/MongooseIM) to use your Pleroma server for user authentication, automatically giving every local user an XMPP account.
In general, you just have to follow the configuration described at [https://mongooseim.readthedocs.io/en/latest/authentication-backends/HTTP-authentication-module/](https://mongooseim.readthedocs.io/en/latest/authentication-backends/HTTP-authentication-module/) and do these changes to your mongooseim.cfg.
1. Set the auth_method to `{auth_method, http}`.
2. Add the http auth pool like this: `{http, global, auth, [{workers, 50}], [{server, "https://yourpleromainstance.com"}]}`
Restart your MongooseIM server, your users should now be able to connect with their Pleroma credentials.

View file

@ -5,11 +5,12 @@ Possible uses include:
* marking incoming messages with media from a given account or instance as sensitive * marking incoming messages with media from a given account or instance as sensitive
* rejecting messages from a specific instance * rejecting messages from a specific instance
* rejecting reports (flags) from a specific instance
* removing/unlisting messages from the public timelines * removing/unlisting messages from the public timelines
* removing media from messages * removing media from messages
* sending only public messages to a specific instance * sending only public messages to a specific instance
The MRF provides user-configurable policies. The default policy is `NoOpPolicy`, which disables the MRF functionality. Pleroma also includes an easy to use policy called `SimplePolicy` which maps messages matching certain pre-defined criterion to actions built into the policy module. The MRF provides user-configurable policies. The default policy is `NoOpPolicy`, which disables the MRF functionality. Pleroma also includes an easy to use policy called `SimplePolicy` which maps messages matching certain pre-defined criterion to actions built into the policy module.
It is possible to use multiple, active MRF policies at the same time. It is possible to use multiple, active MRF policies at the same time.
## Quarantine Instances ## Quarantine Instances
@ -41,12 +42,13 @@ Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_si
* `media_nsfw`: Servers in this group will have the #nsfw tag and sensitive setting injected into incoming messages which contain media. * `media_nsfw`: Servers in this group will have the #nsfw tag and sensitive setting injected into incoming messages which contain media.
* `reject`: Servers in this group will have their messages rejected. * `reject`: Servers in this group will have their messages rejected.
* `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields. * `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields.
* `report_removal`: Servers in this group will have their reports (flags) rejected.
Servers should be configured as lists. Servers should be configured as lists.
### Example ### Example
This example will enable `SimplePolicy`, block media from `illegalporn.biz`, mark media as NSFW from `porn.biz` and `porn.business`, reject messages from `spam.com` and remove messages from `spam.university` from the federated timeline: This example will enable `SimplePolicy`, block media from `illegalporn.biz`, mark media as NSFW from `porn.biz` and `porn.business`, reject messages from `spam.com`, remove messages from `spam.university` from the federated timeline and block reports (flags) from `whiny.whiner`:
``` ```
config :pleroma, :instance, config :pleroma, :instance,
@ -56,7 +58,8 @@ config :pleroma, :mrf_simple,
media_removal: ["illegalporn.biz"], media_removal: ["illegalporn.biz"],
media_nsfw: ["porn.biz", "porn.business"], media_nsfw: ["porn.biz", "porn.business"],
reject: ["spam.com"], reject: ["spam.com"],
federated_timeline_removal: ["spam.university"] federated_timeline_removal: ["spam.university"],
report_removal: ["whiny.whiner"]
``` ```

View file

@ -87,7 +87,7 @@ sudo adduser -S -s /bin/false -h /opt/pleroma -H pleroma
```shell ```shell
sudo mkdir -p /opt/pleroma sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone 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: * Change to the new directory:

View file

@ -66,7 +66,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```shell ```shell
sudo mkdir -p /opt/pleroma sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone 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: * Change to the new directory:

View file

@ -143,7 +143,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```shell ```shell
sudo mkdir -p /opt/pleroma sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone 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: * Change to the new directory:

View file

@ -12,6 +12,7 @@ This guide will assume you are on Debian Stretch. This guide should also work wi
* `erlang-tools` * `erlang-tools`
* `erlang-parsetools` * `erlang-parsetools`
* `erlang-eldap`, if you want to enable ldap authenticator * `erlang-eldap`, if you want to enable ldap authenticator
* `erlang-ssh`
* `erlang-xmerl` * `erlang-xmerl`
* `git` * `git`
* `build-essential` * `build-essential`
@ -49,7 +50,7 @@ sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
```shell ```shell
sudo apt update sudo apt update
sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh
``` ```
### Install PleromaBE ### Install PleromaBE
@ -67,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```shell ```shell
sudo mkdir -p /opt/pleroma sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone 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: * Change to the new directory:

View file

@ -14,6 +14,7 @@
- erlang-dev - erlang-dev
- erlang-tools - erlang-tools
- erlang-parsetools - erlang-parsetools
- erlang-ssh
- erlang-xmerl (Jessieではバックポートからインストールすること) - erlang-xmerl (Jessieではバックポートからインストールすること)
- git - git
- build-essential - build-essential
@ -44,7 +45,7 @@ wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
* ElixirとErlangをインストールします、 * ElixirとErlangをインストールします、
``` ```
apt update && apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools apt update && apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh
``` ```
### Pleroma BE (バックエンド) をインストールします ### Pleroma BE (バックエンド) をインストールします
@ -68,7 +69,7 @@ cd ~
* Gitリポジトリをクローンします。 * Gitリポジトリをクローンします。
``` ```
git clone https://git.pleroma.social/pleroma/pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma
``` ```
* 新しいディレクトリに移動します。 * 新しいディレクトリに移動します。

View file

@ -106,7 +106,7 @@ It is highly recommended you use your own fork for the `https://path/to/repo` pa
```shell ```shell
pleroma$ cd ~ pleroma$ cd ~
pleroma$ git clone https://path/to/repo pleroma$ git clone -b master https://path/to/repo
``` ```
* Change to the new directory: * Change to the new directory:

View file

@ -58,7 +58,7 @@ Clone the repository:
``` ```
$ cd /home/pleroma $ 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: Configure Pleroma. Note that you need a domain name at this point:

View file

@ -29,7 +29,7 @@ This creates a "pleroma" login class and sets higher values than default for dat
Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): `useradd -m -L pleroma _pleroma` Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): `useradd -m -L pleroma _pleroma`
#### Clone pleroma's directory #### Clone pleroma's directory
Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone 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 #### Postgresql
Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql: Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql:

View file

@ -44,7 +44,7 @@ Vaihda pleroma-käyttäjään ja mene kotihakemistoosi:
Lataa pleroman lähdekoodi: 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` `$ cd pleroma`

View file

@ -1,30 +1,30 @@
# Introduction to Pleroma # Introduction to Pleroma
## What is Pleroma? ## What is Pleroma?
Pleroma is a federated social networking platform, compatible with GNU social, Mastodon and other OStatus and ActivityPub implementations. It is free software licensed under the AGPLv3. Pleroma is a federated social networking platform, compatible with GNU social, Mastodon and other OStatus and ActivityPub implementations. It is free software licensed under the AGPLv3.
It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing. It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing.
It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other. It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other.
One account on a instance is enough to talk to the entire fediverse! One account on a instance is enough to talk to the entire fediverse!
## How can I use it? ## How can I use it?
Pleroma instances are already widely deployed, a list can be found here: Pleroma instances are already widely deployed, a list can be found here:
http://distsn.org/pleroma-instances.html http://distsn.org/pleroma-instances.html
If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too! If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too!
Installation instructions can be found here: Installation instructions can be found here:
[main Pleroma wiki](/) [main Pleroma wiki](/)
## I got an account, now what? ## I got an account, now what?
Great! Now you can explore the fediverse! Great! Now you can explore the fediverse!
- Open the login page for your Pleroma instance (for ex. https://pleroma.soykaf.com) and login with your username and password. - Open the login page for your Pleroma instance (for ex. https://pleroma.soykaf.com) and login with your username and password.
(If you don't have one yet, click on Register) :slightly_smiling_face: (If you don't have one yet, click on Register) :slightly_smiling_face:
At this point you will have two columns in front of you. At this point you will have two columns in front of you.
### Left column ### Left column
- first block: here you can see your avatar, your nickname a bio, and statistics (Statuses, Following, Followers). - first block: here you can see your avatar, your nickname a bio, and statistics (Statuses, Following, Followers).
Under that you have a text form which allows you to post new statuses. The icon on the left is for uploading media files and attach them to your post. The number under the text form is a character counter, every instance can have a different character limit (the default is 5000). Under that you have a text form which allows you to post new statuses. The icon on the left is for uploading media files and attach them to your post. The number under the text form is a character counter, every instance can have a different character limit (the default is 5000).
If you want to mention someone, type @ + name of the person. A drop-down menu will help you in finding the right person. :slight_smile: If you want to mention someone, type @ + name of the person. A drop-down menu will help you in finding the right person. :slight_smile:
To post your status, simply press Submit. To post your status, simply press Submit.
- second block: Here you can switch between the different timelines: - second block: Here you can switch between the different timelines:
@ -38,7 +38,7 @@ To post your status, simply press Submit.
- fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses. - fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses.
### Right column ### Right column
This is where the interesting stuff happens! :slight_smile: This is where the interesting stuff happens! :slight_smile:
Depending on the timeline you will see different statuses, but each status has a standard structure: Depending on the timeline you will see different statuses, but each status has a standard structure:
- Icon + name + link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the replied-to status). - Icon + name + link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the replied-to status).
- A + button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime! - A + button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime!
@ -47,9 +47,9 @@ Depending on the timeline you will see different statuses, but each status has a
- Four buttons (left to right): Reply, Repeat, Favorite, Delete. - Four buttons (left to right): Reply, Repeat, Favorite, Delete.
## Mastodon interface ## Mastodon interface
If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! :smile: If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! :smile:
Just add a "/web" after your instance url (for ex. https://pleroma.soycaf.com/web) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! :fireworks: Just add a "/web" after your instance url (for ex. https://pleroma.soycaf.com/web) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! :fireworks:
For more information on the Mastodon interface, please look here: For more information on the Mastodon interface, please look here:
https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md
Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma. Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma.

2
elixir_buildpack.config Normal file
View file

@ -0,0 +1,2 @@
elixir_version=1.8.2
erlang_version=21.3.7

View file

@ -10,7 +10,9 @@ example.tld {
gzip 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 websocket
transparent transparent
} }

View file

@ -0,0 +1,45 @@
#!/bin/sh
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
project_id="74"
project_branch="rebase/glitch-soc"
static_dir="instance/static"
# For bundling:
# project_branch="pleroma"
# static_dir="priv/static"
if [[ ! -d "${static_dir}" ]]
then
echo "Error: ${static_dir} directory is missing, are you sure you are running this script at the root of pleromas repository?"
exit 1
fi
last_modified="$(curl -s -I 'https://git.pleroma.social/api/v4/projects/'${project_id}'/jobs/artifacts/'${project_branch}'/download?job=build' | grep '^Last-Modified:' | cut -d: -f2-)"
echo "branch:${project_branch}"
echo "Last-Modified:${last_modified}"
artifact="mastofe.zip"
if [[ -e mastofe.timestamp ]] && [[ "${last_modified}" != "" ]]
then
if [[ "$(cat mastofe.timestamp)" == "${last_modified}" ]]
then
echo "MastoFE is up-to-date, exiting…"
exit 0
fi
fi
curl -c - "https://git.pleroma.social/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=build" -o "${artifact}" || exit
# TODO: Update the emoji as well
rm -fr "${static_dir}/sw.js" "${static_dir}/packs" || exit
unzip -q "${artifact}" || exit
cp public/assets/sw.js "${static_dir}/sw.js" || exit
cp -r public/packs "${static_dir}/packs" || exit
echo "${last_modified}" > mastofe.timestamp
rm -fr public
rm -i "${artifact}"

View file

@ -58,8 +58,10 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
RewriteRule /(.*) ws://localhost:4000/$1 [P,L] RewriteRule /(.*) ws://localhost:4000/$1 [P,L]
ProxyRequests off ProxyRequests off
ProxyPass / http://localhost:4000/ # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
ProxyPassReverse / http://localhost:4000/ # 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} RequestHeader set Host ${servername}
ProxyPreserveHost On ProxyPreserveHost On

View 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, "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"}
% ]}
% ]},
{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, "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"},
% {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:
%%%.

View file

@ -69,7 +69,9 @@ server {
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host; 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; client_max_body_size 16m;
} }

View file

@ -1,4 +1,4 @@
vcl 4.0; vcl 4.1;
import std; import std;
backend default { backend default {
@ -35,24 +35,6 @@ sub vcl_recv {
} }
return(purge); 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 { sub vcl_backend_response {
@ -61,6 +43,12 @@ sub vcl_backend_response {
set beresp.do_gzip = true; 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 # CHUNKED SUPPORT
if (bereq.http.x-range ~ "bytes=" && beresp.status == 206) { if (bereq.http.x-range ~ "bytes=" && beresp.status == 206) {
set beresp.ttl = 10m; set beresp.ttl = 10m;
@ -73,8 +61,6 @@ sub vcl_backend_response {
return (deliver); return (deliver);
} }
# Default object caching of 86400s;
set beresp.ttl = 86400s;
# Allow serving cached content for 6h in case backend goes down # Allow serving cached content for 6h in case backend goes down
set beresp.grace = 6h; set beresp.grace = 6h;
@ -90,20 +76,6 @@ sub vcl_backend_response {
set beresp.ttl = 30s; set beresp.ttl = 30s;
return (deliver); 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 # The synthetic response for 301 redirects
@ -132,10 +104,32 @@ sub vcl_hash {
} }
sub vcl_backend_fetch { 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 # CHUNKED SUPPORT
if (bereq.http.x-range) { if (bereq.http.x-range) {
set bereq.http.Range = 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 { sub vcl_deliver {
@ -145,3 +139,9 @@ sub vcl_deliver {
unset resp.http.CR; unset resp.http.CR;
} }
} }
sub vcl_backend_error {
# Retry broken backend responses.
set bereq.http.X-Varnish-Backend-503 = "1";
return (retry);
}

View file

@ -29,13 +29,13 @@ def system_info do
end end
defp assign_db_info(healthcheck) do defp assign_db_info(healthcheck) do
database = Application.get_env(:pleroma, Repo)[:database] database = Pleroma.Config.get([Repo, :database])
query = query =
"select state, count(pid) from pg_stat_activity where datname = '#{database}' group by state;" "select state, count(pid) from pg_stat_activity where datname = '#{database}' group by state;"
result = Repo.query!(query) result = Repo.query!(query)
pool_size = Application.get_env(:pleroma, Repo)[:pool_size] pool_size = Pleroma.Config.get([Repo, :pool_size])
db_info = db_info =
Enum.reduce(result.rows, %{active: 0, idle: 0}, fn [state, cnt], states -> Enum.reduce(result.rows, %{active: 0, idle: 0}, fn [state, cnt], states ->

View file

@ -0,0 +1,25 @@
defmodule Mix.Tasks.Pleroma.Benchmark do
use Mix.Task
alias Mix.Tasks.Pleroma.Common
def run(["search"]) do
Common.start_pleroma()
Benchee.run(%{
"search" => fn ->
Pleroma.Web.MastodonAPI.MastodonAPIController.status_search(nil, "cofe")
end
})
end
def run(["tag"]) do
Common.start_pleroma()
Benchee.run(%{
"tag" => fn ->
%{"type" => "Create", "tag" => "cofe"}
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
end
})
end
end

View file

@ -4,6 +4,10 @@
defmodule Mix.Tasks.Pleroma.Database do defmodule Mix.Tasks.Pleroma.Database do
alias Mix.Tasks.Pleroma.Common alias Mix.Tasks.Pleroma.Common
alias Pleroma.Conversation
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
require Logger require Logger
use Mix.Task use Mix.Task
@ -19,6 +23,18 @@ defmodule Mix.Tasks.Pleroma.Database do
Options: Options:
- `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references - `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references
## Prune old objects from the database
mix pleroma.database prune_objects
## Create a conversation for all existing DMs. Can be safely re-run.
mix pleroma.database bump_all_conversations
## Remove duplicated items from following and update followers count for all users
mix pleroma.database update_users_following_followers_counts
""" """
def run(["remove_embedded_objects" | args]) do def run(["remove_embedded_objects" | args]) do
{options, [], []} = {options, [], []} =
@ -32,7 +48,7 @@ def run(["remove_embedded_objects" | args]) do
Common.start_pleroma() Common.start_pleroma()
Logger.info("Removing embedded objects") Logger.info("Removing embedded objects")
Pleroma.Repo.query!( Repo.query!(
"update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;", "update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
[], [],
timeout: :infinity timeout: :infinity
@ -41,7 +57,62 @@ def run(["remove_embedded_objects" | args]) do
if Keyword.get(options, :vacuum) do if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL") Logger.info("Runnning VACUUM FULL")
Pleroma.Repo.query!( Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
end
end
def run(["bump_all_conversations"]) do
Common.start_pleroma()
Conversation.bump_for_all_activities()
end
def run(["update_users_following_followers_counts"]) do
Common.start_pleroma()
users = Repo.all(User)
Enum.each(users, &User.remove_duplicated_following/1)
Enum.each(users, &User.update_follower_count/1)
end
def run(["prune_objects" | args]) do
import Ecto.Query
{options, [], []} =
OptionParser.parse(
args,
strict: [
vacuum: :boolean
]
)
Common.start_pleroma()
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days])
Logger.info("Pruning objects older than #{deadline} days")
time_deadline =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(-(deadline * 86_400))
public = "https://www.w3.org/ns/activitystreams#Public"
from(o in Object,
where: fragment("?->'to' \\? ? OR ?->'cc' \\? ?", o.data, ^public, o.data, ^public),
where: o.inserted_at < ^time_deadline,
where:
fragment("split_part(?->>'actor', '/', 3) != ?", o.data, ^Pleroma.Web.Endpoint.host())
)
|> Repo.delete_all(timeout: :infinity)
if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL")
Repo.query!(
"vacuum full;", "vacuum full;",
[], [],
timeout: :infinity timeout: :infinity

View file

@ -109,7 +109,7 @@ def run(["get-packs" | args]) do
]) ])
) )
binary_archive = Tesla.get!(src_url).body binary_archive = Tesla.get!(client(), src_url).body
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16() archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright] sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
@ -137,7 +137,7 @@ def run(["get-packs" | args]) do
]) ])
) )
files = Tesla.get!(files_url).body |> Poison.decode!() files = Tesla.get!(client(), files_url).body |> Jason.decode!()
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name])) IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
@ -213,7 +213,7 @@ def run(["gen-pack", src]) do
IO.puts("Downloading the pack and generating SHA256") IO.puts("Downloading the pack and generating SHA256")
binary_archive = Tesla.get!(src).body binary_archive = Tesla.get!(client(), src).body
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16() archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
IO.puts("SHA256 is #{archive_sha}") IO.puts("SHA256 is #{archive_sha}")
@ -239,7 +239,7 @@ def run(["gen-pack", src]) do
emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts) emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)
File.write!(files_name, Poison.encode!(emoji_map, pretty: true)) File.write!(files_name, Jason.encode!(emoji_map, pretty: true))
IO.puts(""" IO.puts("""
@ -248,11 +248,11 @@ def run(["gen-pack", src]) do
""") """)
if File.exists?("index.json") do if File.exists?("index.json") do
existing_data = File.read!("index.json") |> Poison.decode!() existing_data = File.read!("index.json") |> Jason.decode!()
File.write!( File.write!(
"index.json", "index.json",
Poison.encode!( Jason.encode!(
Map.merge( Map.merge(
existing_data, existing_data,
pack_json pack_json
@ -263,16 +263,16 @@ def run(["gen-pack", src]) do
IO.puts("index.json file has been update with the #{name} pack") IO.puts("index.json file has been update with the #{name} pack")
else else
File.write!("index.json", Poison.encode!(pack_json, pretty: true)) File.write!("index.json", Jason.encode!(pack_json, pretty: true))
IO.puts("index.json has been created with the #{name} pack") IO.puts("index.json has been created with the #{name} pack")
end end
end end
defp fetch_manifest(from) do defp fetch_manifest(from) do
Poison.decode!( Jason.decode!(
if String.starts_with?(from, "http") do if String.starts_with?(from, "http") do
Tesla.get!(from).body Tesla.get!(client(), from).body
else else
File.read!(from) File.read!(from)
end end
@ -290,4 +290,12 @@ defp parse_global_opts(args) do
] ]
) )
end end
defp client do
middleware = [
{Tesla.Middleware.FollowRedirects, [max_redirects: 3]}
]
Tesla.client(middleware)
end
end end

View file

@ -77,6 +77,10 @@ defmodule Mix.Tasks.Pleroma.User do
## Delete tags from a user. ## Delete tags from a user.
mix pleroma.user untag NICKNAME TAGS mix pleroma.user untag NICKNAME TAGS
## Toggle confirmation of the user's account.
mix pleroma.user toggle_confirmed NICKNAME
""" """
def run(["new", nickname, email | rest]) do def run(["new", nickname, email | rest]) do
{options, [], []} = {options, [], []} =
@ -126,7 +130,7 @@ def run(["new", nickname, email | rest]) do
proceed? = assume_yes? or Mix.shell().yes?("Continue?") proceed? = assume_yes? or Mix.shell().yes?("Continue?")
unless not proceed? do if proceed? do
Common.start_pleroma() Common.start_pleroma()
params = %{ params = %{
@ -138,7 +142,7 @@ def run(["new", nickname, email | rest]) do
bio: bio bio: bio
} }
changeset = User.register_changeset(%User{}, params, confirmed: true) changeset = User.register_changeset(%User{}, params, need_confirmation: false)
{:ok, _user} = User.register(changeset) {:ok, _user} = User.register(changeset)
Mix.shell().info("User #{nickname} created") Mix.shell().info("User #{nickname} created")
@ -163,7 +167,7 @@ def run(["rm", nickname]) do
Common.start_pleroma() Common.start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete(user) User.perform(:delete, user)
Mix.shell().info("User #{nickname} deleted.") Mix.shell().info("User #{nickname} deleted.")
else else
_ -> _ ->
@ -380,7 +384,7 @@ def run(["delete_activities", nickname]) do
Common.start_pleroma() Common.start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete_user_activities(user) {:ok, _} = User.delete_user_activities(user)
Mix.shell().info("User #{nickname} statuses deleted.") Mix.shell().info("User #{nickname} statuses deleted.")
else else
_ -> _ ->
@ -388,6 +392,21 @@ def run(["delete_activities", nickname]) do
end end
end end
def run(["toggle_confirmed", nickname]) do
Common.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.")
else
_ ->
Mix.shell().error("No local user #{nickname}")
end
end
defp set_moderator(user, value) do defp set_moderator(user, value) do
info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value}) info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value})

View file

@ -6,14 +6,19 @@ defmodule Pleroma.Activity do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.ThreadMute
alias Pleroma.User
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@type actor :: String.t()
@primary_key {:id, Pleroma.FlakeId, autogenerate: true} @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19 # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
@ -33,6 +38,9 @@ defmodule Pleroma.Activity do
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
field(:actor, :string) field(:actor, :string)
field(:recipients, {:array, :string}, default: []) field(:recipients, {:array, :string}, default: [])
field(:thread_muted?, :boolean, virtual: true)
# This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark
has_one(:bookmark, Bookmark)
has_many(:notifications, Notification, on_delete: :delete_all) has_many(:notifications, Notification, on_delete: :delete_all)
# Attention: this is a fake relation, don't try to preload it blindly and expect it to work! # Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
@ -54,23 +62,46 @@ defmodule Pleroma.Activity do
timestamps() timestamps()
end end
def with_preloaded_object(query) do def with_joined_object(query) do
query join(query, :inner, [activity], o in Object,
|> join(
:inner,
[activity],
o in Object,
on: on:
fragment( fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data, o.data,
activity.data, activity.data,
activity.data activity.data
) ),
as: :object
) )
|> preload([activity, object], object: object)
end end
def with_preloaded_object(query) do
query
|> has_named_binding?(:object)
|> if(do: query, else: with_joined_object(query))
|> preload([activity, object: object], object: object)
end
def with_preloaded_bookmark(query, %User{} = user) do
from([a] in query,
left_join: b in Bookmark,
on: b.user_id == ^user.id and b.activity_id == a.id,
preload: [bookmark: b]
)
end
def with_preloaded_bookmark(query, _), do: query
def with_set_thread_muted_field(query, %User{} = user) do
from([a] in query,
left_join: tm in ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
select: %Activity{a | thread_muted?: not is_nil(tm.id)}
)
end
def with_set_thread_muted_field(query, _), do: query
def get_by_ap_id(ap_id) do def get_by_ap_id(ap_id) do
Repo.one( Repo.one(
from( from(
@ -80,9 +111,19 @@ def get_by_ap_id(ap_id) do
) )
end end
def get_bookmark(%Activity{} = activity, %User{} = user) do
if Ecto.assoc_loaded?(activity.bookmark) do
activity.bookmark
else
Bookmark.get(user.id, activity.id)
end
end
def get_bookmark(_, _), do: nil
def change(struct, params \\ %{}) do def change(struct, params \\ %{}) do
struct struct
|> cast(params, [:data]) |> cast(params, [:data, :recipients])
|> validate_required([:data]) |> validate_required([:data])
|> unique_constraint(:ap_id, name: :activities_unique_apid_index) |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
end end
@ -106,7 +147,10 @@ def get_by_ap_id_with_object(ap_id) do
end end
def get_by_id(id) do def get_by_id(id) do
Repo.get(Activity, id) Activity
|> where([a], a.id == ^id)
|> restrict_deactivated_users()
|> Repo.one()
end end
def get_by_id_with_object(id) do def get_by_id_with_object(id) do
@ -174,6 +218,7 @@ def get_all_create_by_object_ap_id(ap_id) do
def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
create_by_object_ap_id(ap_id) create_by_object_ap_id(ap_id)
|> restrict_deactivated_users()
|> Repo.one() |> Repo.one()
end end
@ -260,4 +305,42 @@ def all_by_actor_and_id(actor, status_ids) do
|> where([s], s.actor == ^actor) |> where([s], s.actor == ^actor)
|> Repo.all() |> Repo.all()
end end
def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
from(
a in Activity,
where:
fragment(
"? ->> 'type' = 'Follow'",
a.data
),
where:
fragment(
"? ->> 'state' = 'pending'",
a.data
),
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
a.data,
a.data,
^ap_id
)
)
end
@spec query_by_actor(actor()) :: Ecto.Query.t()
def query_by_actor(actor) do
from(a in Activity, where: a.actor == ^actor)
end
def restrict_deactivated_users(query) do
from(activity in query,
where:
fragment(
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
activity.actor
)
)
end
end end

View file

@ -110,6 +110,7 @@ def start(_type, _args) do
hackney_pool_children() ++ hackney_pool_children() ++
[ [
worker(Pleroma.Web.Federator.RetryQueue, []), worker(Pleroma.Web.Federator.RetryQueue, []),
worker(Pleroma.Web.OAuth.Token.CleanWorker, []),
worker(Pleroma.Stats, []), worker(Pleroma.Stats, []),
worker(Task, [&Pleroma.Web.Push.init/0], restart: :temporary, id: :web_push_init), 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) worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary, id: :federator_init)
@ -131,19 +132,22 @@ def start(_type, _args) do
defp setup_instrumenters do defp setup_instrumenters do
require Prometheus.Registry require Prometheus.Registry
:ok = if Application.get_env(:prometheus, Pleroma.Repo.Instrumenter) do
:telemetry.attach( :ok =
"prometheus-ecto", :telemetry.attach(
[:pleroma, :repo, :query], "prometheus-ecto",
&Pleroma.Repo.Instrumenter.handle_event/4, [:pleroma, :repo, :query],
%{} &Pleroma.Repo.Instrumenter.handle_event/4,
) %{}
)
Pleroma.Repo.Instrumenter.setup()
end
Prometheus.Registry.register_collector(:prometheus_process_collector) Prometheus.Registry.register_collector(:prometheus_process_collector)
Pleroma.Web.Endpoint.MetricsExporter.setup() Pleroma.Web.Endpoint.MetricsExporter.setup()
Pleroma.Web.Endpoint.PipelineInstrumenter.setup() Pleroma.Web.Endpoint.PipelineInstrumenter.setup()
Pleroma.Web.Endpoint.Instrumenter.setup() Pleroma.Web.Endpoint.Instrumenter.setup()
Pleroma.Repo.Instrumenter.setup()
end end
def enabled_hackney_pools do def enabled_hackney_pools do

View file

@ -0,0 +1,16 @@
defmodule Pleroma.BBS.Authenticator do
use Sshd.PasswordAuthenticator
alias Comeonin.Pbkdf2
alias Pleroma.User
def authenticate(username, password) do
username = to_string(username)
password = to_string(password)
with %User{} = user <- User.get_by_nickname(username) do
Pbkdf2.checkpw(password, user.password_hash)
else
_e -> false
end
end
end

146
lib/pleroma/bbs/handler.ex Normal file
View file

@ -0,0 +1,146 @@
defmodule Pleroma.BBS.Handler do
use Sshd.ShellHandler
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
def on_shell(username, _pubkey, _ip, _port) do
:ok = IO.puts("Welcome to #{Pleroma.Config.get([:instance, :name])}!")
user = Pleroma.User.get_cached_by_nickname(to_string(username))
Logger.debug("#{inspect(user)}")
loop(run_state(user: user))
end
def on_connect(username, ip, port, method) do
Logger.debug(fn ->
"""
Incoming SSH shell #{inspect(self())} requested for #{username} from #{inspect(ip)}:#{
inspect(port)
} using #{inspect(method)}
"""
end)
end
def on_disconnect(username, ip, port) do
Logger.debug(fn ->
"Disconnecting SSH shell for #{username} from #{inspect(ip)}:#{inspect(port)}"
end)
end
defp loop(state) do
self_pid = self()
counter = state.counter
prefix = state.prefix
user = state.user
input = spawn(fn -> io_get(self_pid, prefix, counter, user.nickname) end)
wait_input(state, input)
end
def puts_activity(activity) do
status = Pleroma.Web.MastodonAPI.StatusView.render("status.json", %{activity: activity})
IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})")
IO.puts(HtmlSanitizeEx.strip_tags(status.content))
IO.puts("")
end
def handle_command(state, "help") do
IO.puts("Available commands:")
IO.puts("help - This help")
IO.puts("home - Show the home timeline")
IO.puts("p <text> - Post the given text")
IO.puts("r <id> <text> - Reply to the post with the given id")
IO.puts("quit - Quit")
state
end
def handle_command(%{user: user} = state, "r " <> text) do
text = String.trim(text)
[activity_id, rest] = String.split(text, " ", parts: 2)
with %Activity{} <- Activity.get_by_id(activity_id),
{:ok, _activity} <-
CommonAPI.post(user, %{"status" => rest, "in_reply_to_status_id" => activity_id}) do
IO.puts("Replied!")
else
_e -> IO.puts("Could not reply...")
end
state
end
def handle_command(%{user: user} = state, "p " <> text) do
text = String.trim(text)
with {:ok, _activity} <- CommonAPI.post(user, %{"status" => text}) do
IO.puts("Posted!")
else
_e -> IO.puts("Could not post...")
end
state
end
def handle_command(state, "home") do
user = state.user
params =
%{}
|> Map.put("type", ["Create"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
activities =
[user.ap_id | user.following]
|> ActivityPub.fetch_activities(params)
Enum.each(activities, fn activity ->
puts_activity(activity)
end)
state
end
def handle_command(state, command) do
IO.puts("Unknown command '#{command}'")
state
end
defp wait_input(state, input) do
receive do
{:input, ^input, "quit\n"} ->
IO.puts("Exiting...")
{:input, ^input, code} when is_binary(code) ->
code = String.trim(code)
state = handle_command(state, code)
loop(%{state | counter: state.counter + 1})
{:error, :interrupted} ->
IO.puts("Caught Ctrl+C...")
loop(%{state | counter: state.counter + 1})
{:input, ^input, msg} ->
:ok = Logger.warn("received unknown message: #{inspect(msg)}")
loop(%{state | counter: state.counter + 1})
end
end
defp run_state(opts) do
%{prefix: "pleroma", counter: 1, user: opts[:user]}
end
defp io_get(pid, prefix, counter, username) do
prompt = prompt(prefix, counter, username)
send(pid, {:input, self(), IO.gets(:stdio, prompt)})
end
defp prompt(prefix, counter, username) do
prompt = "#{username}@#{prefix}:#{counter}>"
prompt <> " "
end
end

View file

@ -15,7 +15,7 @@ def new do
%{error: "Kocaptcha service unavailable"} %{error: "Kocaptcha service unavailable"}
{:ok, res} -> {:ok, res} ->
json_resp = Poison.decode!(res.body) json_resp = Jason.decode!(res.body)
%{ %{
type: :kocaptcha, type: :kocaptcha,

View file

@ -12,8 +12,12 @@ def get(key), do: get(key, nil)
def get([key], default), do: get(key, default) def get([key], default), do: get(key, default)
def get([parent_key | keys], default) do def get([parent_key | keys], default) do
Application.get_env(:pleroma, parent_key) case :pleroma
|> get_in(keys) || default |> Application.get_env(parent_key)
|> get_in(keys) do
nil -> default
any -> any
end
end end
def get(key, default) do def get(key, default) do

View file

@ -5,15 +5,6 @@
defmodule Pleroma.Config.DeprecationWarnings do defmodule Pleroma.Config.DeprecationWarnings do
require Logger require Logger
def check_frontend_config_mechanism do
if Pleroma.Config.get(:fe) do
Logger.warn("""
!!!DEPRECATION WARNING!!!
You are using the old configuration mechanism for the frontend. Please check config.md.
""")
end
end
def check_hellthread_threshold do def check_hellthread_threshold do
if Pleroma.Config.get([:mrf_hellthread, :threshold]) do if Pleroma.Config.get([:mrf_hellthread, :threshold]) do
Logger.warn(""" Logger.warn("""
@ -24,7 +15,6 @@ def check_hellthread_threshold do
end end
def warn do def warn do
check_frontend_config_mechanism()
check_hellthread_threshold() check_hellthread_threshold()
end end
end end

View file

@ -0,0 +1,92 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation do
alias Pleroma.Conversation.Participation
alias Pleroma.Repo
alias Pleroma.User
use Ecto.Schema
import Ecto.Changeset
schema "conversations" do
# This is the context ap id.
field(:ap_id, :string)
has_many(:participations, Participation)
has_many(:users, through: [:participations, :user])
timestamps()
end
def creation_cng(struct, params) do
struct
|> cast(params, [:ap_id])
|> validate_required([:ap_id])
|> unique_constraint(:ap_id)
end
def create_for_ap_id(ap_id) do
%__MODULE__{}
|> creation_cng(%{ap_id: ap_id})
|> Repo.insert(
on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]],
returning: true,
conflict_target: :ap_id
)
end
def get_for_ap_id(ap_id) do
Repo.get_by(__MODULE__, ap_id: ap_id)
end
@doc """
This will
1. Create a conversation if there isn't one already
2. Create a participation for all the people involved who don't have one already
3. Bump all relevant participations to 'unread'
"""
def create_or_bump_for(activity, opts \\ []) do
with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
"Create" <- activity.data["type"],
object <- Pleroma.Object.normalize(activity),
true <- object.data["type"] in ["Note", "Question"],
ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
{:ok, conversation} = create_for_ap_id(ap_id)
users = User.get_users_from_set(activity.recipients, false)
participations =
Enum.map(users, fn user ->
{:ok, participation} =
Participation.create_for_user_and_conversation(user, conversation, opts)
participation
end)
{:ok,
%{
conversation
| participations: participations
}}
else
e -> {:error, e}
end
end
@doc """
This is only meant to be run by a mix task. It creates conversations/participations for all direct messages in the database.
"""
def bump_for_all_activities do
stream =
Pleroma.Web.ActivityPub.ActivityPub.fetch_direct_messages_query()
|> Repo.stream()
Repo.transaction(
fn ->
stream
|> Enum.each(fn a -> create_or_bump_for(a, read: true) end)
end,
timeout: :infinity
)
end
end

View file

@ -0,0 +1,83 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation.Participation do
use Ecto.Schema
alias Pleroma.Conversation
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
import Ecto.Changeset
import Ecto.Query
schema "conversation_participations" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:conversation, Conversation)
field(:read, :boolean, default: false)
field(:last_activity_id, Pleroma.FlakeId, virtual: true)
timestamps()
end
def creation_cng(struct, params) do
struct
|> cast(params, [:user_id, :conversation_id, :read])
|> validate_required([:user_id, :conversation_id])
end
def create_for_user_and_conversation(user, conversation, opts \\ []) do
read = !!opts[:read]
%__MODULE__{}
|> creation_cng(%{user_id: user.id, conversation_id: conversation.id, read: read})
|> Repo.insert(
on_conflict: [set: [read: read, updated_at: NaiveDateTime.utc_now()]],
returning: true,
conflict_target: [:user_id, :conversation_id]
)
end
def read_cng(struct, params) do
struct
|> cast(params, [:read])
|> validate_required([:read])
end
def mark_as_read(participation) do
participation
|> read_cng(%{read: true})
|> Repo.update()
end
def mark_as_unread(participation) do
participation
|> read_cng(%{read: false})
|> Repo.update()
end
def for_user(user, params \\ %{}) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
order_by: [desc: p.updated_at]
)
|> Pleroma.Pagination.fetch_paginated(params)
|> Repo.preload(conversation: [:users])
end
def for_user_with_last_activity_id(user, params \\ %{}) do
for_user(user, params)
|> Enum.map(fn participation ->
activity_id =
ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{
"user" => user,
"blocking_user" => user
})
%{
participation
| last_activity_id: activity_id
}
end)
end
end

View file

@ -29,7 +29,7 @@ def report(to, reporter, account, statuses, comment) do
end end
statuses_html = statuses_html =
if length(statuses) > 0 do if is_list(statuses) && length(statuses) > 0 do
statuses_list_html = statuses_list_html =
statuses statuses
|> Enum.map(fn |> Enum.map(fn

View file

@ -22,7 +22,7 @@ defmodule Pleroma.Emoji do
@ets __MODULE__.Ets @ets __MODULE__.Ets
@ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}] @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
@groups Application.get_env(:pleroma, :emoji)[:groups] @groups Pleroma.Config.get([:emoji, :groups])
@doc false @doc false
def start_link do def start_link do
@ -112,7 +112,7 @@ defp load do
# Compat thing for old custom emoji handling & default emoji, # Compat thing for old custom emoji handling & default emoji,
# it should run even if there are no emoji packs # 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 = emojis =
(load_from_file("config/emoji.txt") ++ (load_from_file("config/emoji.txt") ++

View file

@ -38,7 +38,8 @@ def get_filters(%User{id: user_id} = _user) do
query = query =
from( from(
f in Pleroma.Filter, f in Pleroma.Filter,
where: f.user_id == ^user_id where: f.user_id == ^user_id,
order_by: [desc: :id]
) )
Repo.all(query) Repo.all(query)

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Formatter do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
@safe_mention_regex ~r/^(\s*(?<mentions>@.+?\s+)+)(?<rest>.*)/ @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 @link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/ @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
@ -113,9 +113,7 @@ def emojify(text, emoji, strip \\ false) do
html = html =
if not strip do if not strip do
"<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{ "<img class='emoji' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />"
MediaProxy.url(file)
}' />"
else else
"" ""
end end
@ -130,12 +128,23 @@ def demojify(text) do
def demojify(text, nil), do: text def demojify(text, nil), do: text
@doc "Outputs a list of the emoji-shortcodes in a text"
def get_emoji(text) when is_binary(text) do def get_emoji(text) when is_binary(text) do
Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end) Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end)
end end
def get_emoji(_), do: [] def get_emoji(_), do: []
@doc "Outputs a list of the emoji-Maps in a text"
def get_emoji_map(text) when is_binary(text) do
get_emoji(text)
|> Enum.reduce(%{}, fn {name, file, _group}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
end
def get_emoji_map(_), do: []
def html_escape({text, mentions, hashtags}, type) do def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags} {html_escape(text, type), mentions, hashtags}
end end

View file

@ -77,13 +77,13 @@ def render_activities(activities) do
user = User.get_cached_by_ap_id(activity.data["actor"]) user = User.get_cached_by_ap_id(activity.data["actor"])
object = Object.normalize(activity) object = Object.normalize(activity)
like_count = object["like_count"] || 0 like_count = object.data["like_count"] || 0
announcement_count = object["announcement_count"] || 0 announcement_count = object.data["announcement_count"] || 0
link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <> link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <>
info("#{like_count} likes, #{announcement_count} repeats") <> info("#{like_count} likes, #{announcement_count} repeats") <>
"i\tfake\t(NULL)\t0\r\n" <> "i\tfake\t(NULL)\t0\r\n" <>
info(HTML.strip_tags(String.replace(object["content"], "<br>", "\r"))) info(HTML.strip_tags(String.replace(object.data["content"], "<br>", "\r")))
end) end)
|> Enum.join("i\tfake\t(NULL)\t0\r\n") |> Enum.join("i\tfake\t(NULL)\t0\r\n")
end end

View file

@ -28,12 +28,18 @@ def filter_tags(html, scrubber), do: Scrubber.scrub(html, scrubber)
def filter_tags(html), do: filter_tags(html, nil) def filter_tags(html), do: filter_tags(html, nil)
def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags) def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags)
def get_cached_scrubbed_html_for_activity(content, scrubbers, activity, key \\ "") do def get_cached_scrubbed_html_for_activity(
content,
scrubbers,
activity,
key \\ "",
callback \\ fn x -> x end
) do
key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}" key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
Cachex.fetch!(:scrubber_cache, key, fn _key -> Cachex.fetch!(:scrubber_cache, key, fn _key ->
object = Pleroma.Object.normalize(activity) object = Pleroma.Object.normalize(activity)
ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false) ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
end) end)
end end
@ -42,24 +48,27 @@ def get_cached_stripped_html_for_activity(content, activity, key) do
content, content,
HtmlSanitizeEx.Scrubber.StripTags, HtmlSanitizeEx.Scrubber.StripTags,
activity, activity,
key key,
&HtmlEntities.decode/1
) )
end end
def ensure_scrubbed_html( def ensure_scrubbed_html(
content, content,
scrubbers, scrubbers,
false = _fake fake,
callback
) do ) do
{:commit, filter_tags(content, scrubbers)} content =
end content
|> filter_tags(scrubbers)
|> callback.()
def ensure_scrubbed_html( if fake do
content, {:ignore, content}
scrubbers, else
true = _fake {:commit, content}
) do end
{:ignore, filter_tags(content, scrubbers)}
end end
defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do
@ -95,7 +104,6 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
paragraphs, breaks and links are allowed through the filter. paragraphs, breaks and links are allowed through the filter.
""" """
@markup Application.get_env(:pleroma, :markup)
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
require HtmlSanitizeEx.Scrubber.Meta require HtmlSanitizeEx.Scrubber.Meta
@ -133,15 +141,14 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.allow_tag_with_these_attributes("span", []) Meta.allow_tag_with_these_attributes("span", [])
# allow inline images for custom emoji # allow inline images for custom emoji
@allow_inline_images Keyword.get(@markup, :allow_inline_images) if Pleroma.Config.get([:markup, :allow_inline_images]) do
if @allow_inline_images do
# restrict img tags to http/https only, because of MediaProxy. # restrict img tags to http/https only, because of MediaProxy.
Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"]) Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"])
Meta.allow_tag_with_these_attributes("img", [ Meta.allow_tag_with_these_attributes("img", [
"width", "width",
"height", "height",
"class",
"title", "title",
"alt" "alt"
]) ])
@ -158,7 +165,6 @@ defmodule Pleroma.HTML.Scrubber.Default do
# credo:disable-for-previous-line # credo:disable-for-previous-line
# No idea how to fix this one… # No idea how to fix this one…
@markup Application.get_env(:pleroma, :markup)
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
Meta.remove_cdata_sections_before_scrub() Meta.remove_cdata_sections_before_scrub()
@ -203,7 +209,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"]) Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
Meta.allow_tag_with_these_attributes("span", []) 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 if @allow_inline_images do
# restrict img tags to http/https only, because of MediaProxy. # restrict img tags to http/https only, because of MediaProxy.
@ -212,14 +218,13 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("img", [ Meta.allow_tag_with_these_attributes("img", [
"width", "width",
"height", "height",
"class",
"title", "title",
"alt" "alt"
]) ])
end end
@allow_tables Keyword.get(@markup, :allow_tables) if Pleroma.Config.get([:markup, :allow_tables]) do
if @allow_tables do
Meta.allow_tag_with_these_attributes("table", []) Meta.allow_tag_with_these_attributes("table", [])
Meta.allow_tag_with_these_attributes("tbody", []) Meta.allow_tag_with_these_attributes("tbody", [])
Meta.allow_tag_with_these_attributes("td", []) Meta.allow_tag_with_these_attributes("td", [])
@ -228,9 +233,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("tr", []) Meta.allow_tag_with_these_attributes("tr", [])
end end
@allow_headings Keyword.get(@markup, :allow_headings) if Pleroma.Config.get([:markup, :allow_headings]) do
if @allow_headings do
Meta.allow_tag_with_these_attributes("h1", []) Meta.allow_tag_with_these_attributes("h1", [])
Meta.allow_tag_with_these_attributes("h2", []) Meta.allow_tag_with_these_attributes("h2", [])
Meta.allow_tag_with_these_attributes("h3", []) Meta.allow_tag_with_these_attributes("h3", [])
@ -238,9 +241,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("h5", []) Meta.allow_tag_with_these_attributes("h5", [])
end end
@allow_fonts Keyword.get(@markup, :allow_fonts) if Pleroma.Config.get([:markup, :allow_fonts]) do
if @allow_fonts do
Meta.allow_tag_with_these_attributes("font", ["face"]) Meta.allow_tag_with_these_attributes("font", ["face"])
end end

View file

@ -8,7 +8,7 @@ defmodule Pleroma.HTTP.Connection do
""" """
@hackney_options [ @hackney_options [
connect_timeout: 2_000, connect_timeout: 10_000,
recv_timeout: 20_000, recv_timeout: 20_000,
follow_redirect: true, follow_redirect: true,
pool: :federation pool: :federation
@ -32,9 +32,11 @@ def new(opts \\ []) do
defp hackney_options(opts) do defp hackney_options(opts) do
options = Keyword.get(opts, :adapter, []) options = Keyword.get(opts, :adapter, [])
adapter_options = Pleroma.Config.get([:http, :adapter], []) adapter_options = Pleroma.Config.get([:http, :adapter], [])
proxy_url = Pleroma.Config.get([:http, :proxy_url], nil)
@hackney_options @hackney_options
|> Keyword.merge(adapter_options) |> Keyword.merge(adapter_options)
|> Keyword.merge(options) |> Keyword.merge(options)
|> Keyword.merge(proxy: proxy_url)
end end
end end

View file

@ -65,12 +65,9 @@ defp process_sni_options(options, url) do
end end
def process_request_options(options) do def process_request_options(options) do
config = Application.get_env(:pleroma, :http, []) case Pleroma.Config.get([:http, :proxy_url]) do
proxy = Keyword.get(config, :proxy_url, nil)
case proxy do
nil -> options nil -> options
_ -> options ++ [proxy: proxy] proxy -> options ++ [proxy: proxy]
end end
end end

View file

@ -45,8 +45,15 @@ def url(request, u) do
Add headers to the request Add headers to the request
""" """
@spec headers(map(), list(tuple)) :: map() @spec headers(map(), list(tuple)) :: map()
def headers(request, h) do def headers(request, header_list) do
Map.put_new(request, :headers, h) header_list =
if Pleroma.Config.get([:http, :send_user_agent]) do
header_list ++ [{"User-Agent", Pleroma.Application.user_agent()}]
else
header_list
end
Map.put_new(request, :headers, header_list)
end end
@doc """ @doc """

44
lib/pleroma/keys.ex Normal file
View file

@ -0,0 +1,44 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Keys do
# Native generation of RSA keys is only available since OTP 20+ and in default build conditions
# We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
try do
_ = :public_key.generate_key({:rsa, 2048, 65_537})
def generate_rsa_pem do
key = :public_key.generate_key({:rsa, 2048, 65_537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
{:ok, pem}
end
rescue
_ ->
def generate_rsa_pem do
port = Port.open({:spawn, "openssl genrsa"}, [:binary])
{:ok, pem} =
receive do
{^port, {:data, pem}} -> {:ok, pem}
end
Port.close(port)
if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
{:ok, pem}
else
:error
end
end
end
def keys_from_pem(pem) do
[private_key_code] = :public_key.pem_decode(pem)
private_key = :public_key.pem_entry_decode(private_key_code)
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
public_key = {:RSAPublicKey, modulus, exponent}
{:ok, private_key, public_key}
end
end

View file

@ -33,6 +33,13 @@ def changeset(%Notification{} = notification, attrs) do
def for_user_query(user) do def for_user_query(user) do
Notification Notification
|> where(user_id: ^user.id) |> where(user_id: ^user.id)
|> where(
[n, a],
fragment(
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
a.actor
)
)
|> join(:inner, [n], activity in assoc(n, :activity)) |> join(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object, |> join(:left, [n, a], object in Object,
on: on:
@ -120,10 +127,15 @@ def dismiss(%{id: user_id} = _user, id) do
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity) def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
when type in ["Create", "Like", "Announce", "Follow"] do when type in ["Create", "Like", "Announce", "Follow"] do
users = get_notified_from_activity(activity) object = Object.normalize(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end) unless object && object.data["type"] == "Answer" do
{:ok, notifications} users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
else
{:ok, []}
end
end end
def create_notifications(_), do: {:ok, []} def create_notifications(_), do: {:ok, []}
@ -159,7 +171,16 @@ def get_notified_from_activity(
def get_notified_from_activity(_, _local_only), do: [] def get_notified_from_activity(_, _local_only), do: []
def skip?(activity, user) 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)) |> Enum.any?(&skip?(&1, activity, user))
end end
@ -172,12 +193,6 @@ def skip?(:blocked, activity, user) do
User.blocks?(user, %{ap_id: actor}) User.blocks?(user, %{ap_id: actor})
end 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 def skip?(:muted, activity, user) do
actor = activity.data["actor"] actor = activity.data["actor"]
@ -194,12 +209,32 @@ def skip?(
User.following?(follower, user) User.following?(follower, user)
end 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 def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do
actor = activity.data["actor"] actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor) followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed) User.following?(user, followed)
end 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 def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
actor = activity.data["actor"] actor = activity.data["actor"]

View file

@ -35,6 +35,9 @@ def change(struct, params \\ %{}) do
|> unique_constraint(:ap_id, name: :objects_unique_apid_index) |> unique_constraint(:ap_id, name: :objects_unique_apid_index)
end 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(nil), do: nil
def get_by_ap_id(ap_id) do def get_by_ap_id(ap_id) do
@ -130,6 +133,13 @@ def delete(%Object{data: %{"id" => id}} = object) do
end end
end end
def prune(%Object{data: %{"id" => id}} = object) do
with {:ok, object} <- Repo.delete(object),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
{:ok, object}
end
end
def set_cache(%Object{data: %{"id" => ap_id}} = object) do def set_cache(%Object{data: %{"id" => ap_id}} = object) do
Cachex.put(:object_cache, "object:#{ap_id}", object) Cachex.put(:object_cache, "object:#{ap_id}", object)
{:ok, object} {:ok, object}
@ -188,4 +198,34 @@ def decrease_replies_count(ap_id) do
_ -> {:error, "Not found"} _ -> {:error, "Not found"}
end end
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 end

View file

@ -1,7 +1,5 @@
defmodule Pleroma.Object.Containment do defmodule Pleroma.Object.Containment do
@moduledoc """ @moduledoc """
# Object Containment
This module contains some useful functions for containing objects to specific This module contains some useful functions for containing objects to specific
origins and determining those origins. They previously lived in the origins and determining those origins. They previously lived in the
ActivityPub `Transmogrifier` module. ActivityPub `Transmogrifier` module.

View file

@ -1,4 +1,5 @@
defmodule Pleroma.Object.Fetcher do defmodule Pleroma.Object.Fetcher do
alias Pleroma.HTTP
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
@ -6,7 +7,18 @@ defmodule Pleroma.Object.Fetcher do
require Logger require Logger
@httpoison Application.get_env(:pleroma, :httpoison) defp reinject_object(data) do
Logger.debug("Reinjecting object #{data["id"]}")
with data <- Transmogrifier.fix_object(data),
{:ok, object} <- Object.create(data) do
{:ok, object}
else
e ->
Logger.error("Error while processing object: #{inspect(e)}")
{:error, e}
end
end
# TODO: # TODO:
# This will create a Create activity, which we need internally at the moment. # This will create a Create activity, which we need internally at the moment.
@ -26,12 +38,17 @@ def fetch_object_from_id(id) do
"object" => data "object" => data
}, },
:ok <- Containment.contain_origin(id, params), :ok <- Containment.contain_origin(id, params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do {:ok, activity} <- Transmogrifier.handle_incoming(params),
{:ok, Object.normalize(activity, false)} {:object, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object}
else else
{:error, {:reject, nil}} -> {:error, {:reject, nil}} ->
{:reject, nil} {:reject, nil}
{:object, data, nil} ->
reinject_object(data)
object = %Object{} -> object = %Object{} ->
{:ok, object} {:ok, object}
@ -60,7 +77,7 @@ def fetch_and_contain_remote_object_from_id(id) do
with true <- String.starts_with?(id, "http"), with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <- {:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get( HTTP.get(
id, id,
[{:Accept, "application/activity+json"}] [{:Accept, "application/activity+json"}]
), ),

View file

@ -0,0 +1,31 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do
import Plug.Conn
alias Pleroma.Config
alias Pleroma.User
def init(options) do
options
end
def call(conn, _) do
public? = Config.get!([:instance, :public])
case {public?, conn} do
{true, _} ->
conn
{false, %{assigns: %{user: %User{}}}} ->
conn
{false, _} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{error: "This resource requires authentication."}))
|> halt
end
end
end

View file

@ -10,7 +10,7 @@ def init(options) do
end end
def call(conn, _opts) do def call(conn, _opts) do
if Keyword.get(Application.get_env(:pleroma, :instance), :federating) do if Pleroma.Config.get([:instance, :federating]) do
conn conn
else else
conn conn

View file

@ -20,8 +20,9 @@ def call(conn, _options) do
defp headers do defp headers do
referrer_policy = Config.get([:http_security, :referrer_policy]) referrer_policy = Config.get([:http_security, :referrer_policy])
report_uri = Config.get([:http_security, :report_uri])
[ headers = [
{"x-xss-protection", "1; mode=block"}, {"x-xss-protection", "1; mode=block"},
{"x-permitted-cross-domain-policies", "none"}, {"x-permitted-cross-domain-policies", "none"},
{"x-frame-options", "DENY"}, {"x-frame-options", "DENY"},
@ -30,12 +31,27 @@ defp headers do
{"x-download-options", "noopen"}, {"x-download-options", "noopen"},
{"content-security-policy", csp_string() <> ";"} {"content-security-policy", csp_string() <> ";"}
] ]
if report_uri do
report_group = %{
"group" => "csp-endpoint",
"max-age" => 10_886_400,
"endpoints" => [
%{"url" => report_uri}
]
}
headers ++ [{"reply-to", Jason.encode!(report_group)}]
else
headers
end
end end
defp csp_string do defp csp_string do
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
static_url = Pleroma.Web.Endpoint.static_url() static_url = Pleroma.Web.Endpoint.static_url()
websocket_url = String.replace(static_url, "http", "ws") websocket_url = Pleroma.Web.Endpoint.websocket_url()
report_uri = Config.get([:http_security, :report_uri])
connect_src = "connect-src 'self' #{static_url} #{websocket_url}" connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
@ -53,7 +69,7 @@ defp csp_string do
"script-src 'self'" "script-src 'self'"
end end
[ main_part = [
"default-src 'none'", "default-src 'none'",
"base-uri 'self'", "base-uri 'self'",
"frame-ancestors 'none'", "frame-ancestors 'none'",
@ -63,11 +79,14 @@ defp csp_string do
"font-src 'self'", "font-src 'self'",
"manifest-src 'self'", "manifest-src 'self'",
connect_src, connect_src,
script_src, script_src
if scheme == "https" do
"upgrade-insecure-requests"
end
] ]
report = if report_uri, do: ["report-uri #{report_uri}; report-to csp-endpoint"], else: []
insecure = if scheme == "https", do: ["upgrade-insecure-requests"], else: []
(main_part ++ report ++ insecure)
|> Enum.join("; ") |> Enum.join("; ")
end end

View file

@ -4,7 +4,6 @@
defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.HTTPSignatures
import Plug.Conn import Plug.Conn
require Logger require Logger

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Plugs.OAuthPlug do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
@realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
@ -16,14 +17,45 @@ def init(options), do: options
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(conn, _) do def call(%{params: %{"access_token" => access_token}} = conn, _) do
with {:ok, token_str} <- fetch_token_str(conn), with {:ok, user, token_record} <- fetch_user_and_token(access_token) do
{:ok, user, token_record} <- fetch_user_and_token(token_str) do
conn conn
|> assign(:token, token_record) |> assign(:token, token_record)
|> assign(:user, user) |> assign(:user, user)
else else
_ -> conn _ ->
# token found, but maybe only with app
with {:ok, app, token_record} <- fetch_app_and_token(access_token) do
conn
|> assign(:token, token_record)
|> assign(:app, app)
else
_ -> conn
end
end
end
def call(conn, _) do
case fetch_token_str(conn) do
{:ok, token} ->
with {:ok, user, token_record} <- fetch_user_and_token(token) do
conn
|> assign(:token, token_record)
|> assign(:user, user)
else
_ ->
# token found, but maybe only with app
with {:ok, app, token_record} <- fetch_app_and_token(token) do
conn
|> assign(:token, token_record)
|> assign(:app, app)
else
_ -> conn
end
end
_ ->
conn
end end
end end
@ -44,6 +76,16 @@ defp fetch_user_and_token(token) do
end end
end end
@spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil
defp fetch_app_and_token(token) do
query =
from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app])
with %Token{app: app} = token_record <- Repo.one(query) do
{:ok, app, token_record}
end
end
# Gets token from session by :oauth_token key # Gets token from session by :oauth_token key
# #
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}

View file

@ -0,0 +1,36 @@
# 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

View file

@ -19,4 +19,32 @@ defmodule Instrumenter do
def init(_, opts) do def init(_, opts) do
{:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
end end
@doc "find resource based on prepared query"
@spec find_resource(Ecto.Query.t()) :: {:ok, struct()} | {:error, :not_found}
def find_resource(%Ecto.Query{} = query) do
case __MODULE__.one(query) do
nil -> {:error, :not_found}
resource -> {:ok, resource}
end
end
def find_resource(_query), do: {:error, :not_found}
@doc """
Gets association from cache or loads if need
## Examples
iex> Repo.get_assoc(token, :user)
%User{}
"""
@spec get_assoc(struct(), atom()) :: {:ok, struct()} | {:error, :not_found}
def get_assoc(resource, association) do
case __MODULE__.preload(resource, association) do
%{^association => assoc} when not is_nil(assoc) -> {:ok, assoc}
_ -> {:error, :not_found}
end
end
end end

View file

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy do defmodule Pleroma.ReverseProxy do
alias Pleroma.HTTP
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++ @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
~w(if-unmodified-since if-none-match if-range range) ~w(if-unmodified-since if-none-match if-range range)
@resp_cache_headers ~w(etag date last-modified cache-control) @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). * `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 [] @default_hackney_options []
@inline_content_types [ @inline_content_types [
@ -97,7 +96,7 @@ def call(conn = %{method: method}, url, opts) when method in @methods do
hackney_opts = hackney_opts =
@default_hackney_options @default_hackney_options
|> Keyword.merge(Keyword.get(opts, :http, [])) |> Keyword.merge(Keyword.get(opts, :http, []))
|> @httpoison.process_request_options() |> HTTP.process_request_options()
req_headers = build_req_headers(conn.req_headers, opts) 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)}") Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
method = method |> String.downcase() |> String.to_existing_atom() 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, headers, client} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers), client} {:ok, code, downcase_headers(headers), client}
@ -197,7 +196,7 @@ defp chunk_reply(conn, client, opts, sent_so_far, duration) do
duration, duration,
Keyword.get(opts, :max_read_duration, @max_read_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), {:ok, duration} <- increase_read_duration(duration),
sent_so_far = sent_so_far + byte_size(data), sent_so_far = sent_so_far + byte_size(data),
:ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)), :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)),

40
lib/pleroma/signature.ex Normal file
View 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.Signature do
@behaviour HTTPSignatures.Adapter
alias Pleroma.Keys
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
def fetch_public_key(conn) do
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def refetch_public_key(conn) do
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def sign(%User{} = user, headers) do
with {:ok, %{info: %{keys: keys}}} <- User.ensure_keys_present(user),
{:ok, private_key, _} <- Keys.keys_from_pem(keys) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
end
end
end

View file

@ -34,7 +34,7 @@ def schedule_update do
def update_stats do def update_stats do
peers = peers =
from( from(
u in Pleroma.User, u in User,
select: fragment("distinct split_part(?, '@', 2)", u.nickname), select: fragment("distinct split_part(?, '@', 2)", u.nickname),
where: u.local != ^true where: u.local != ^true
) )
@ -44,10 +44,13 @@ def update_stats do
domain_count = Enum.count(peers) domain_count = Enum.count(peers)
status_query = status_query =
from(u in User.local_user_query(), select: fragment("sum((?->>'note_count')::int)", u.info)) from(u in User.Query.build(%{local: true}),
select: fragment("sum((?->>'note_count')::int)", u.info)
)
status_count = Repo.one(status_query) status_count = Repo.one(status_query)
user_count = Repo.aggregate(User.active_local_user_query(), :count, :id)
user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id)
Agent.update(__MODULE__, fn _ -> Agent.update(__MODULE__, fn _ ->
{peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}} {peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}}

View file

@ -4,7 +4,7 @@
defmodule Pleroma.Upload do defmodule Pleroma.Upload do
@moduledoc """ @moduledoc """
# Upload Manage user uploads
Options: Options:
* `:type`: presets for activity type (defaults to Document) and size limits from app configuration * `:type`: presets for activity type (defaults to Document) and size limits from app configuration

View file

@ -4,11 +4,10 @@
defmodule Pleroma.Uploaders.MDII do defmodule Pleroma.Uploaders.MDII do
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.HTTP
@behaviour Pleroma.Uploaders.Uploader @behaviour Pleroma.Uploaders.Uploader
@httpoison Application.get_env(:pleroma, :httpoison)
# MDII-hosted images are never passed through the MediaPlug; only local media. # MDII-hosted images are never passed through the MediaPlug; only local media.
# Delegate to Pleroma.Uploaders.Local # Delegate to Pleroma.Uploaders.Local
def get_file(file) do def get_file(file) do
@ -25,7 +24,7 @@ def put_file(upload) do
query = "#{cgi}?#{extension}" query = "#{cgi}?#{extension}"
with {:ok, %{status: 200, body: body}} <- 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() remote_file_name = String.split(body) |> List.first()
public_url = "#{files}/#{remote_file_name}.#{extension}" public_url = "#{files}/#{remote_file_name}.#{extension}"
{:ok, {:url, public_url}} {:ok, {:url, public_url}}

View file

@ -14,7 +14,7 @@ def process_url(url) do
def process_response_body(body) do def process_response_body(body) do
body body
|> Poison.decode!() |> Jason.decode!()
end end
def get_token do def get_token do
@ -38,7 +38,7 @@ def get_token do
end end
def make_auth_body(username, password, tenant) do def make_auth_body(username, password, tenant) do
Poison.encode!(%{ Jason.encode!(%{
:auth => %{ :auth => %{
:passwordCredentials => %{ :passwordCredentials => %{
:username => username, :username => username,

View file

@ -10,8 +10,7 @@ defmodule Pleroma.User do
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Bookmark alias Pleroma.Keys
alias Pleroma.Formatter
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Registration alias Pleroma.Registration
@ -55,10 +54,9 @@ defmodule Pleroma.User do
field(:search_type, :integer, virtual: true) field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: []) field(:tags, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec) field(:last_refreshed_at, :naive_datetime_usec)
has_many(:bookmarks, Bookmark)
has_many(:notifications, Notification) has_many(:notifications, Notification)
has_many(:registrations, Registration) has_many(:registrations, Registration)
embeds_one(:info, Pleroma.User.Info) embeds_one(:info, User.Info)
timestamps() timestamps()
end end
@ -108,10 +106,8 @@ def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
def user_info(%User{} = user) do def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0
%{ %{
following_count: length(user.following) - oneself, following_count: following_count(user),
note_count: user.info.note_count, note_count: user.info.note_count,
follower_count: user.info.follower_count, follower_count: user.info.follower_count,
locked: user.info.locked, locked: user.info.locked,
@ -120,6 +116,20 @@ def user_info(%User{} = user) do
} }
end end
def restrict_deactivated(query) do
from(u in query,
where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
)
end
def following_count(%User{following: []}), do: 0
def following_count(%User{} = user) do
user
|> get_friends_query()
|> Repo.aggregate(:count, :id)
end
def remote_user_creation(params) do def remote_user_creation(params) do
params = params =
params params
@ -157,7 +167,7 @@ def remote_user_creation(params) do
def update_changeset(struct, params \\ %{}) do def update_changeset(struct, params \\ %{}) do
struct struct
|> cast(params, [:bio, :name, :avatar]) |> cast(params, [:bio, :name, :avatar, :following])
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex()) |> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: 5000) |> validate_length(:bio, max: 5000)
@ -207,14 +217,15 @@ def reset_password(user, data) do
end end
def register_changeset(struct, params \\ %{}, opts \\ []) do def register_changeset(struct, params \\ %{}, opts \\ []) do
confirmation_status = need_confirmation? =
if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do if is_nil(opts[:need_confirmation]) do
:confirmed Pleroma.Config.get([:instance, :account_activation_required])
else else
:unconfirmed opts[:need_confirmation]
end end
info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status) info_change =
User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
changeset = changeset =
struct struct
@ -223,7 +234,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|> validate_confirmation(:password) |> validate_confirmation(:password)
|> unique_constraint(:email) |> unique_constraint(:email)
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames])) |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex()) |> validate_format(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex) |> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000) |> validate_length(:bio, max: 1000)
@ -257,10 +268,7 @@ defp autofollow_users(user) do
candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
autofollowed_users = autofollowed_users =
from(u in User, User.Query.build(%{nickname: candidates, local: true, deactivated: false})
where: u.local == true,
where: u.nickname in ^candidates
)
|> Repo.all() |> Repo.all()
follow_all(user, autofollowed_users) follow_all(user, autofollowed_users)
@ -271,7 +279,7 @@ def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset), with {:ok, user} <- Repo.insert(changeset),
{:ok, user} <- autofollow_users(user), {:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user), {:ok, user} <- set_cache(user),
{:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user), {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(user) do {:ok, _} <- try_send_confirmation_email(user) do
{:ok, user} {:ok, user}
end end
@ -358,9 +366,7 @@ def follow_all(follower, followeds) do
end end
def follow(%User{} = follower, %User{info: info} = followed) do def follow(%User{} = follower, %User{info: info} = followed) do
user_config = Application.get_env(:pleroma, :user) deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
ap_followers = followed.follower_address ap_followers = followed.follower_address
cond do cond do
@ -418,24 +424,6 @@ def following?(%User{} = follower, %User{} = followed) do
Enum.member?(follower.following, followed.follower_address) Enum.member?(follower.following, followed.follower_address)
end end
def follow_import(%User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
Enum.map(
followed_identifiers,
fn followed_identifier ->
with %User{} = followed <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do
followed
else
err ->
Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
err
end
end
)
end
def locked?(%User{} = user) do def locked?(%User{} = user) do
user.info.locked || false user.info.locked || false
end end
@ -507,7 +495,15 @@ def get_cached_by_id(id) do
def get_cached_by_nickname(nickname) do def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}" key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
Cachex.fetch!(:user_cache, key, fn ->
user_result = get_or_fetch_by_nickname(nickname)
case user_result do
{:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil}
end
end)
end end
def get_cached_by_nickname_or_id(nickname_or_id) do def get_cached_by_nickname_or_id(nickname_or_id) do
@ -543,47 +539,37 @@ def fetch_by_nickname(nickname) do
def get_or_fetch_by_nickname(nickname) do def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do with %User{} = user <- get_by_nickname(nickname) do
user {:ok, user}
else else
_e -> _e ->
with [_nick, _domain] <- String.split(nickname, "@"), with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do {:ok, user} <- fetch_by_nickname(nickname) do
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
# TODO turn into job fetch_initial_posts(user)
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end end
user {:ok, user}
else else
_e -> nil _e -> {:error, "not found " <> nickname}
end end
end end
end end
@doc "Fetch some posts when the user has just been federated with" @doc "Fetch some posts when the user has just been federated with"
def fetch_initial_posts(user) do def fetch_initial_posts(user),
pages = Pleroma.Config.get!([:fetch_initial_posts, :pages]) do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
Enum.each( @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
# Insert all the posts in reverse order, so they're in the right order on the timeline def get_followers_query(%User{} = user, nil) do
Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)), User.Query.build(%{followers: user, deactivated: false})
&Pleroma.Web.Federator.incoming_ap_doc/1
)
end
def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
from(
u in User,
where: fragment("? <@ ?", ^[follower_address], u.following),
where: u.id != ^id
)
end end
def get_followers_query(user, page) do def get_followers_query(user, page) do
from(u in get_followers_query(user, nil)) from(u in get_followers_query(user, nil))
|> paginate(page, 20) |> User.Query.paginate(page, 20)
end end
@spec get_followers_query(User.t()) :: Ecto.Query.t()
def get_followers_query(user), do: get_followers_query(user, nil) def get_followers_query(user), do: get_followers_query(user, nil)
def get_followers(user, page \\ nil) do def get_followers(user, page \\ nil) do
@ -598,19 +584,17 @@ def get_followers_ids(user, page \\ nil) do
Repo.all(from(u in q, select: u.id)) Repo.all(from(u in q, select: u.id))
end end
def get_friends_query(%User{id: id, following: following}, nil) do @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
from( def get_friends_query(%User{} = user, nil) do
u in User, User.Query.build(%{friends: user, deactivated: false})
where: u.follower_address in ^following,
where: u.id != ^id
)
end end
def get_friends_query(user, page) do def get_friends_query(user, page) do
from(u in get_friends_query(user, nil)) from(u in get_friends_query(user, nil))
|> paginate(page, 20) |> User.Query.paginate(page, 20)
end end
@spec get_friends_query(User.t()) :: Ecto.Query.t()
def get_friends_query(user), do: get_friends_query(user, nil) def get_friends_query(user), do: get_friends_query(user, nil)
def get_friends(user, page \\ nil) do def get_friends(user, page \\ nil) do
@ -625,33 +609,10 @@ def get_friends_ids(user, page \\ nil) do
Repo.all(from(u in q, select: u.id)) Repo.all(from(u in q, select: u.id))
end end
def get_follow_requests_query(%User{} = user) do @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
from(
a in Activity,
where:
fragment(
"? ->> 'type' = 'Follow'",
a.data
),
where:
fragment(
"? ->> 'state' = 'pending'",
a.data
),
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
a.data,
a.data,
^user.ap_id
)
)
end
def get_follow_requests(%User{} = user) do def get_follow_requests(%User{} = user) do
users = users =
user Activity.follow_requests_for_actor(user)
|> User.get_follow_requests_query()
|> join(:inner, [a], u in User, on: a.actor == u.ap_id) |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
|> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address])) |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
|> group_by([a, u], u.id) |> group_by([a, u], u.id)
@ -715,18 +676,15 @@ def update_note_count(%User{} = user) do
info_cng = User.Info.set_note_count(user.info, note_count) info_cng = User.Info.set_note_count(user.info, note_count)
cng = user
change(user) |> change()
|> put_embed(:info, info_cng) |> put_embed(:info, info_cng)
|> update_and_set_cache()
update_and_set_cache(cng)
end end
def update_follower_count(%User{} = user) do def update_follower_count(%User{} = user) do
follower_count_query = follower_count_query =
User User.Query.build(%{followers: user, deactivated: false})
|> where([u], ^user.follower_address in u.following)
|> where([u], u.id != ^user.id)
|> select([u], %{count: count(u.id)}) |> select([u], %{count: count(u.id)})
User User
@ -750,38 +708,31 @@ def update_follower_count(%User{} = user) do
end end
end end
def get_users_from_set_query(ap_ids, false) do def remove_duplicated_following(%User{following: following} = user) do
from( uniq_following = Enum.uniq(following)
u in User,
where: u.ap_id in ^ap_ids if length(following) == length(uniq_following) do
) {:ok, user}
end else
user
def get_users_from_set_query(ap_ids, true) do |> update_changeset(%{following: uniq_following})
query = get_users_from_set_query(ap_ids, false) |> update_and_set_cache()
end
from(
u in query,
where: u.local == true
)
end end
@spec get_users_from_set([String.t()], boolean()) :: [User.t()]
def get_users_from_set(ap_ids, local_only \\ true) do def get_users_from_set(ap_ids, local_only \\ true) do
get_users_from_set_query(ap_ids, local_only) criteria = %{ap_id: ap_ids, deactivated: false}
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
User.Query.build(criteria)
|> Repo.all() |> Repo.all()
end end
@spec get_recipients_from_activity(Activity.t()) :: [User.t()]
def get_recipients_from_activity(%Activity{recipients: to}) do def get_recipients_from_activity(%Activity{recipients: to}) do
query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
from( |> Repo.all()
u in User,
where: u.ap_id in ^to,
or_where: fragment("? && ?", u.following, ^to)
)
query = from(u in query, where: u.local == true)
Repo.all(query)
end end
def search(query, resolve \\ false, for_user \\ nil) do def search(query, resolve \\ false, for_user \\ nil) do
@ -807,7 +758,7 @@ def search_query(query, for_user) do
from(s in subquery(boost_search_rank_query(distinct_query, for_user)), from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
order_by: [desc: s.search_rank], order_by: [desc: s.search_rank],
limit: 20 limit: 40
) )
end end
@ -878,6 +829,7 @@ defp fts_search_subquery(term, query \\ User) do
^processed_query ^processed_query
) )
) )
|> restrict_deactivated()
end end
defp trigram_search_subquery(term) do defp trigram_search_subquery(term) do
@ -896,23 +848,7 @@ defp trigram_search_subquery(term) do
}, },
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term) where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
) )
end |> restrict_deactivated()
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
Enum.map(
blocked_identifiers,
fn blocked_identifier ->
with %User{} = blocked <- get_or_fetch(blocked_identifier),
{:ok, blocker} <- block(blocker, blocked),
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked
else
err ->
Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
err
end
end
)
end end
def mute(muter, %User{ap_id: ap_id}) do def mute(muter, %User{ap_id: ap_id}) do
@ -1043,14 +979,23 @@ def subscribed_to?(user, %{ap_id: ap_id}) do
end end
end end
def muted_users(user), @spec muted_users(User.t()) :: [User.t()]
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes)) def muted_users(user) do
User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
|> Repo.all()
end
def blocked_users(user), @spec blocked_users(User.t()) :: [User.t()]
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks)) def blocked_users(user) do
User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
|> Repo.all()
end
def subscribers(user), @spec subscribers(User.t()) :: [User.t()]
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers)) def subscribers(user) do
User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
|> Repo.all()
end
def block_domain(user, domain) do def block_domain(user, domain) do
info_cng = info_cng =
@ -1076,77 +1021,25 @@ def unblock_domain(user, domain) do
update_and_set_cache(cng) update_and_set_cache(cng)
end end
def maybe_local_user_query(query, local) do def deactivate_async(user, status \\ true) do
if local, do: local_user_query(query), else: query PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
end
def local_user_query(query \\ User) do
from(
u in query,
where: u.local == true,
where: not is_nil(u.nickname)
)
end
def maybe_external_user_query(query, external) do
if external, do: external_user_query(query), else: query
end
def external_user_query(query \\ User) do
from(
u in query,
where: u.local == false,
where: not is_nil(u.nickname)
)
end
def maybe_active_user_query(query, active) do
if active, do: active_user_query(query), else: query
end
def active_user_query(query \\ User) do
from(
u in query,
where: fragment("not (?->'deactivated' @> 'true')", u.info),
where: not is_nil(u.nickname)
)
end
def maybe_deactivated_user_query(query, deactivated) do
if deactivated, do: deactivated_user_query(query), else: query
end
def deactivated_user_query(query \\ User) do
from(
u in query,
where: fragment("(?->'deactivated' @> 'true')", u.info),
where: not is_nil(u.nickname)
)
end
def active_local_user_query do
from(
u in local_user_query(),
where: fragment("not (?->'deactivated' @> 'true')", u.info)
)
end
def moderator_user_query do
from(
u in User,
where: u.local == true,
where: fragment("?->'is_moderator' @> 'true'", u.info)
)
end end
def deactivate(%User{} = user, status \\ true) do def deactivate(%User{} = user, status \\ true) do
info_cng = User.Info.set_activation_status(user.info, status) info_cng = User.Info.set_activation_status(user.info, status)
cng = with {:ok, friends} <- User.get_friends(user),
change(user) {:ok, followers} <- User.get_followers(user),
|> put_embed(:info, info_cng) {:ok, user} <-
user
|> change()
|> put_embed(:info, info_cng)
|> update_and_set_cache() do
Enum.each(followers, &invalidate_cache(&1))
Enum.each(friends, &update_follower_count(&1))
update_and_set_cache(cng) {:ok, user}
end
end end
def update_notification_settings(%User{} = user, settings \\ %{}) do def update_notification_settings(%User{} = user, settings \\ %{}) do
@ -1157,7 +1050,12 @@ def update_notification_settings(%User{} = user, settings \\ %{}) do
|> update_and_set_cache() |> update_and_set_cache()
end end
def delete(%User{} = user) do @spec delete(User.t()) :: :ok
def delete(%User{} = user),
do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
{:ok, user} = User.deactivate(user) {:ok, user} = User.deactivate(user)
# Remove all relationships # Remove all relationships
@ -1172,23 +1070,92 @@ def delete(%User{} = user) do
delete_user_activities(user) delete_user_activities(user)
end end
def delete_user_activities(%User{ap_id: ap_id} = user) do @spec perform(atom(), User.t()) :: {:ok, User.t()}
Activity def perform(:fetch_initial_posts, %User{} = user) do
|> where(actor: ^ap_id) pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
|> Activity.with_preloaded_object()
|> Repo.all()
|> Enum.each(fn
%{data: %{"type" => "Create"}} = activity ->
activity |> Object.normalize() |> ActivityPub.delete()
# TODO: Do something with likes, follows, repeats. Enum.each(
_ -> # Insert all the posts in reverse order, so they're in the right order on the timeline
"Doing nothing" Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
end) &Pleroma.Web.Federator.incoming_ap_doc/1
)
{:ok, user} {:ok, user}
end end
def perform(:deactivate_async, user, status), do: deactivate(user, status)
@spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
when is_list(blocked_identifiers) do
Enum.map(
blocked_identifiers,
fn blocked_identifier ->
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, blocker} <- block(blocker, blocked),
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked
else
err ->
Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
err
end
end
)
end
@spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
def perform(:follow_import, %User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
Enum.map(
followed_identifiers,
fn followed_identifier ->
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do
followed
else
err ->
Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
err
end
end
)
end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
do:
PleromaJobQueue.enqueue(:background, __MODULE__, [
:blocks_import,
blocker,
blocked_identifiers
])
def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
do:
PleromaJobQueue.enqueue(:background, __MODULE__, [
:follow_import,
follower,
followed_identifiers
])
def delete_user_activities(%User{ap_id: ap_id} = user) do
stream =
ap_id
|> Activity.query_by_actor()
|> Repo.stream()
Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
{:ok, user}
end
defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
Object.normalize(activity) |> ActivityPub.delete()
end
defp delete_activity(_activity), do: "Doing nothing"
def html_filter_policy(%User{info: %{no_rich_text: true}}) do def html_filter_policy(%User{info: %{no_rich_text: true}}) do
Pleroma.HTML.Scrubber.TwitterText Pleroma.HTML.Scrubber.TwitterText
end end
@ -1202,11 +1169,11 @@ def fetch_by_ap_id(ap_id) do
case ap_try do case ap_try do
{:ok, user} -> {:ok, user} ->
user {:ok, user}
_ -> _ ->
case OStatus.make_user(ap_id) do case OStatus.make_user(ap_id) do
{:ok, user} -> user {:ok, user} -> {:ok, user}
_ -> {:error, "Could not fetch by AP id"} _ -> {:error, "Could not fetch by AP id"}
end end
end end
@ -1216,20 +1183,20 @@ def get_or_fetch_by_ap_id(ap_id) do
user = get_cached_by_ap_id(ap_id) user = get_cached_by_ap_id(ap_id)
if !is_nil(user) and !User.needs_update?(user) do if !is_nil(user) and !User.needs_update?(user) do
user {:ok, user}
else else
# Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled) # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled]) should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
user = fetch_by_ap_id(ap_id) resp = fetch_by_ap_id(ap_id)
if should_fetch_initial do if should_fetch_initial do
with %User{} = user do with {:ok, %User{} = user} <- resp do
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) fetch_initial_posts(user)
end end
end end
user resp
end end
end end
@ -1271,7 +1238,7 @@ def public_key_from_info(%{magic_key: magic_key}) do
end end
def get_public_key_for_ap_id(ap_id) do def get_public_key_for_ap_id(ap_id) do
with %User{} = user <- get_or_fetch_by_ap_id(ap_id), with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key_from_info(user.info) do {:ok, public_key} <- public_key_from_info(user.info) do
{:ok, public_key} {:ok, public_key}
else else
@ -1295,7 +1262,7 @@ def ap_enabled?(%User{info: info}), do: info.ap_enabled
def ap_enabled?(_), do: false def ap_enabled?(_), do: false
@doc "Gets or fetch a user by uri or nickname." @doc "Gets or fetch a user by uri or nickname."
@spec get_or_fetch(String.t()) :: User.t() @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri) def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname) def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
@ -1323,18 +1290,15 @@ def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
end end
end end
def parse_bio(bio, user \\ %User{info: %{source_data: %{}}}) def parse_bio(bio) when is_binary(bio) and bio != "" do
def parse_bio(nil, _user), do: "" bio
def parse_bio(bio, _user) when bio == "", do: bio |> CommonUtils.format_input("text/plain", mentions_format: :full)
|> elem(0)
end
def parse_bio(bio, user) do def parse_bio(_), do: ""
emoji =
(user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
def parse_bio(bio, user) when is_binary(bio) and bio != "" do
# TODO: get profile URLs other than user.ap_id # TODO: get profile URLs other than user.ap_id
profile_urls = [user.ap_id] profile_urls = [user.ap_id]
@ -1344,9 +1308,10 @@ def parse_bio(bio, user) do
rel: &RelMe.maybe_put_rel_me(&1, profile_urls) rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
) )
|> elem(0) |> elem(0)
|> Formatter.emojify(emoji)
end end
def parse_bio(_, _), do: ""
def tag(user_identifiers, tags) when is_list(user_identifiers) do def tag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn -> Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: tag(user_identifier, tags) for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
@ -1414,23 +1379,66 @@ def error_user(ap_id) do
} }
end end
@spec all_superusers() :: [User.t()]
def all_superusers do def all_superusers do
from( User.Query.build(%{super_users: true, local: true, deactivated: false})
u in User,
where: u.local == true,
where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
)
|> Repo.all() |> Repo.all()
end end
defp paginate(query, page, page_size) do
from(u in query,
limit: ^page_size,
offset: ^((page - 1) * page_size)
)
end
def showing_reblogs?(%User{} = user, %User{} = target) do def showing_reblogs?(%User{} = user, %User{} = target) do
target.ap_id not in user.info.muted_reblogs target.ap_id not in user.info.muted_reblogs
end end
@spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
def toggle_confirmation(%User{} = user) do
need_confirmation? = !user.info.confirmation_pending
info_changeset =
User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
user
|> change()
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
end
def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
mascot
end
def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
# use instance-default
config = Pleroma.Config.get([:assets, :mascots])
default_mascot = Pleroma.Config.get([:assets, :default_mascot])
mascot = Keyword.get(config, default_mascot)
%{
"id" => "default-mascot",
"url" => mascot[:url],
"preview_url" => mascot[:url],
"pleroma" => %{
"mime_type" => mascot[:mime_type]
}
}
end
def ensure_keys_present(user) do
info = user.info
if info.keys do
{:ok, user}
else
{:ok, pem} = Keys.generate_rsa_pem()
info_cng =
info
|> User.Info.set_keys(pem)
cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)
update_and_set_cache(cng)
end
end
end end

View file

@ -8,6 +8,8 @@ defmodule Pleroma.User.Info do
alias Pleroma.User.Info alias Pleroma.User.Info
@type t :: %__MODULE__{}
embedded_schema do embedded_schema do
field(:banner, :map, default: %{}) field(:banner, :map, default: %{})
field(:background, :map, default: %{}) field(:background, :map, default: %{})
@ -40,10 +42,16 @@ defmodule Pleroma.User.Info do
field(:hide_follows, :boolean, default: false) field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true) field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: []) field(:pinned_activities, {:array, :string}, default: [])
field(:flavour, :string, default: nil) field(:mascot, :map, default: nil)
field(:emoji, {:array, :map}, default: [])
field(:notification_settings, :map, field(:notification_settings, :map,
default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true} default: %{
"followers" => true,
"follows" => true,
"non_follows" => true,
"non_followers" => true
}
) )
# Found in the wild # Found in the wild
@ -64,10 +72,15 @@ def set_activation_status(info, deactivated) do
end end
def update_notification_settings(info, settings) do 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 = notification_settings =
info.notification_settings info.notification_settings
|> Map.merge(settings) |> Map.merge(settings)
|> Map.take(["remote", "local", "followers", "follows"]) |> Map.take(["followers", "follows", "non_follows", "non_followers"])
params = %{notification_settings: notification_settings} params = %{notification_settings: notification_settings}
@ -209,21 +222,23 @@ def profile_update(info, params) do
]) ])
end end
def confirmation_changeset(info, :confirmed) do @spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t()
confirmation_changeset(info, %{ def confirmation_changeset(info, opts) do
confirmation_pending: false, need_confirmation? = Keyword.get(opts, :need_confirmation)
confirmation_token: nil
})
end
def confirmation_changeset(info, :unconfirmed) do params =
confirmation_changeset(info, %{ if need_confirmation? do
confirmation_pending: true, %{
confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64() confirmation_pending: true,
}) confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
end }
else
%{
confirmation_pending: false,
confirmation_token: nil
}
end
def confirmation_changeset(info, params) do
cast(info, params, [:confirmation_pending, :confirmation_token]) cast(info, params, [:confirmation_pending, :confirmation_token])
end end
@ -235,12 +250,12 @@ def mastodon_settings_update(info, settings) do
|> validate_required([:settings]) |> validate_required([:settings])
end end
def mastodon_flavour_update(info, flavour) do def mascot_update(info, url) do
params = %{flavour: flavour} params = %{mascot: url}
info info
|> cast(params, [:flavour]) |> cast(params, [:mascot])
|> validate_required([:flavour]) |> validate_required([:mascot])
end end
def set_source_data(info, source_data) do def set_source_data(info, source_data) do

154
lib/pleroma/user/query.ex Normal file
View file

@ -0,0 +1,154 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Query do
@moduledoc """
User query builder module. Builds query from new query or another user query.
## Example:
query = Pleroma.User.Query(%{nickname: "nickname"})
another_query = Pleroma.User.Query.build(query, %{email: "email@example.com"})
Pleroma.Repo.all(query)
Pleroma.Repo.all(another_query)
Adding new rules:
- *ilike criteria*
- add field to @ilike_criteria list
- pass non empty string
- e.g. Pleroma.User.Query.build(%{nickname: "nickname"})
- *equal criteria*
- add field to @equal_criteria list
- pass non empty string
- e.g. Pleroma.User.Query.build(%{email: "email@example.com"})
- *contains criteria*
- add field to @containns_criteria list
- pass values list
- e.g. Pleroma.User.Query.build(%{ap_id: ["http://ap_id1", "http://ap_id2"]})
"""
import Ecto.Query
import Pleroma.Web.AdminAPI.Search, only: [not_empty_string: 1]
alias Pleroma.User
@type criteria ::
%{
query: String.t(),
tags: [String.t()],
name: String.t(),
email: String.t(),
local: boolean(),
external: boolean(),
active: boolean(),
deactivated: boolean(),
is_admin: boolean(),
is_moderator: boolean(),
super_users: boolean(),
followers: User.t(),
friends: User.t(),
recipients_from_activity: [String.t()],
nickname: [String.t()],
ap_id: [String.t()]
}
| %{}
@ilike_criteria [:nickname, :name, :query]
@equal_criteria [:email]
@role_criteria [:is_admin, :is_moderator]
@contains_criteria [:ap_id, :nickname]
@spec build(criteria()) :: Query.t()
def build(query \\ base_query(), criteria) do
prepare_query(query, criteria)
end
@spec paginate(Ecto.Query.t(), pos_integer(), pos_integer()) :: Ecto.Query.t()
def paginate(query, page, page_size) do
from(u in query,
limit: ^page_size,
offset: ^((page - 1) * page_size)
)
end
defp base_query do
from(u in User)
end
defp prepare_query(query, criteria) do
Enum.reduce(criteria, query, &compose_query/2)
end
defp compose_query({key, value}, query)
when key in @ilike_criteria and not_empty_string(value) do
# hack for :query key
key = if key == :query, do: :nickname, else: key
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
end
defp compose_query({key, value}, query)
when key in @equal_criteria and not_empty_string(value) do
where(query, [u], ^[{key, value}])
end
defp compose_query({key, values}, query) when key in @contains_criteria and is_list(values) do
where(query, [u], field(u, ^key) in ^values)
end
defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do
Enum.reduce(tags, query, &prepare_tag_criteria/2)
end
defp compose_query({key, _}, query) when key in @role_criteria do
where(query, [u], fragment("(?->? @> 'true')", u.info, ^to_string(key)))
end
defp compose_query({:super_users, _}, query) do
where(
query,
[u],
fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
)
end
defp compose_query({:local, _}, query), do: location_query(query, true)
defp compose_query({:external, _}, query), do: location_query(query, false)
defp compose_query({:active, _}, query) do
where(query, [u], fragment("not (?->'deactivated' @> 'true')", u.info))
|> where([u], not is_nil(u.nickname))
end
defp compose_query({:deactivated, false}, query) do
User.restrict_deactivated(query)
end
defp compose_query({:deactivated, true}, query) do
where(query, [u], fragment("?->'deactivated' @> 'true'", u.info))
|> where([u], not is_nil(u.nickname))
end
defp compose_query({:followers, %User{id: id, follower_address: follower_address}}, query) do
where(query, [u], fragment("? <@ ?", ^[follower_address], u.following))
|> where([u], u.id != ^id)
end
defp compose_query({:friends, %User{id: id, following: following}}, query) do
where(query, [u], u.follower_address in ^following)
|> where([u], u.id != ^id)
end
defp compose_query({:recipients_from_activity, to}, query) do
where(query, [u], u.ap_id in ^to or fragment("? && ?", u.following, ^to))
end
defp compose_query(_unsupported_param, query), do: query
defp prepare_tag_criteria(tag, query) do
or_where(query, [u], fragment("? = any(?)", ^tag, u.tags))
end
defp location_query(query, local) do
where(query, [u], u.local == ^local)
|> where([u], not is_nil(u.nickname))
end
end

View file

@ -24,7 +24,7 @@ defmodule Pleroma.UserInviteToken do
timestamps() timestamps()
end end
@spec create_invite(map()) :: UserInviteToken.t() @spec create_invite(map()) :: {:ok, UserInviteToken.t()}
def create_invite(params \\ %{}) do def create_invite(params \\ %{}) do
%UserInviteToken{} %UserInviteToken{}
|> cast(params, [:max_use, :expires_at]) |> cast(params, [:max_use, :expires_at])

View file

@ -4,7 +4,7 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Instances alias Pleroma.Conversation
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Fetcher alias Pleroma.Object.Fetcher
@ -14,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
import Ecto.Query import Ecto.Query
@ -23,8 +22,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
require Logger require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
# For Announce activities, we filter the recipients based on following status for any actors # For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary. # that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do defp get_recipients(%{"type" => "Announce"} = data) do
@ -111,6 +108,15 @@ def decrease_replies_count_if_reply(%Object{
def decrease_replies_count_if_reply(_object), do: :noop 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 def insert(map, local \\ true, fake \\ false) when is_map(map) do
with nil <- Activity.normalize(map), with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake), map <- lazy_put_activity_defaults(map, fake),
@ -136,12 +142,17 @@ def insert(map, local \\ true, fake \\ false) when is_map(map) do
activity activity
end end
Task.start(fn -> PleromaJobQueue.enqueue(:background, Pleroma.Web.RichMedia.Helpers, [:fetch, activity])
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end)
Notification.create_notifications(activity) Notification.create_notifications(activity)
participations =
activity
|> Conversation.create_or_bump_for()
|> get_participations()
stream_out(activity) stream_out(activity)
stream_out_participations(participations)
{:ok, activity} {:ok, activity}
else else
%Activity{} = activity -> %Activity{} = activity ->
@ -164,42 +175,59 @@ def insert(map, local \\ true, fake \\ false) when is_map(map) do
end end
end end
defp get_participations({:ok, %{participations: participations}}), do: participations
defp get_participations(_), do: []
def stream_out_participations(participations) do
participations =
participations
|> Repo.preload(:user)
Enum.each(participations, fn participation ->
Pleroma.Web.Streamer.stream("participation", participation)
end)
end
def stream_out(activity) do def stream_out(activity) do
public = "https://www.w3.org/ns/activitystreams#Public" public = "https://www.w3.org/ns/activitystreams#Public"
if activity.data["type"] in ["Create", "Announce", "Delete"] do if activity.data["type"] in ["Create", "Announce", "Delete"] do
object = Object.normalize(activity) object = Object.normalize(activity)
Pleroma.Web.Streamer.stream("user", activity) # Do not stream out poll replies
Pleroma.Web.Streamer.stream("list", activity) 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 if Enum.member?(activity.data["to"], public) do
Pleroma.Web.Streamer.stream("public", activity) Pleroma.Web.Streamer.stream("public", activity)
if activity.local do if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity) Pleroma.Web.Streamer.stream("public:local", activity)
end end
if activity.data["type"] in ["Create"] do if activity.data["type"] in ["Create"] do
object.data object.data
|> Map.get("tag", []) |> Map.get("tag", [])
|> Enum.filter(fn tag -> is_bitstring(tag) end) |> Enum.filter(fn tag -> is_bitstring(tag) end)
|> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
if object.data["attachment"] != [] do if object.data["attachment"] != [] do
Pleroma.Web.Streamer.stream("public:media", activity) Pleroma.Web.Streamer.stream("public:media", activity)
if activity.local do if activity.local do
Pleroma.Web.Streamer.stream("public:local:media", activity) Pleroma.Web.Streamer.stream("public:local:media", activity)
end
end 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 end
else
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 end
end end
@ -218,6 +246,7 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f
{:ok, activity} <- insert(create_data, local, fake), {:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity}, {:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data), _ <- increase_replies_count_if_reply(create_data),
_ <- increase_poll_votes_if_vote(create_data),
# Changing note count prior to enqueuing federation task in order to avoid # Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info # race conditions on updating user.info
{:ok, _actor} <- increase_note_count_if_public(actor, activity), {:ok, _actor} <- increase_note_count_if_public(actor, activity),
@ -382,16 +411,12 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru
end end
def block(blocker, blocked, activity_id \\ nil, local \\ true) do def block(blocker, blocked, activity_id \\ nil, local \\ true) do
ap_config = Application.get_env(:pleroma, :activitypub) outgoing_blocks = Pleroma.Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Keyword.get(ap_config, :unfollow_blocked) unfollow_blocked = Pleroma.Config.get([:activitypub, :unfollow_blocked])
outgoing_blocks = Keyword.get(ap_config, :outgoing_blocks)
with true <- unfollow_blocked do if unfollow_blocked do
follow_activity = fetch_latest_follow(blocker, blocked) follow_activity = fetch_latest_follow(blocker, blocked)
if follow_activity, do: unfollow(blocker, blocked, nil, local)
if follow_activity do
unfollow(blocker, blocked, nil, local)
end
end end
with true <- outgoing_blocks, with true <- outgoing_blocks,
@ -456,35 +481,45 @@ def flag(
end end
end end
def fetch_activities_for_context(context, opts \\ %{}) do defp fetch_activities_for_context_query(context, opts) do
public = ["https://www.w3.org/ns/activitystreams#Public"] public = ["https://www.w3.org/ns/activitystreams#Public"]
recipients = recipients =
if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
query = from(activity in Activity) from(activity in Activity)
|> maybe_preload_objects(opts)
query = |> restrict_blocked(opts)
query |> restrict_recipients(recipients, opts["user"])
|> restrict_blocked(opts) |> where(
|> restrict_recipients(recipients, opts["user"]) [activity],
fragment(
query = "?->>'type' = ? and ?->>'context' = ?",
from( activity.data,
activity in query, "Create",
where: activity.data,
fragment( ^context
"?->>'type' = ? and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
),
order_by: [desc: :id]
) )
|> Activity.with_preloaded_object() )
|> exclude_poll_votes(opts)
|> order_by([activity], desc: activity.id)
end
Repo.all(query) @spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()]
def fetch_activities_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> Repo.all()
end
@spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
Pleroma.FlakeId.t() | nil
def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
|> limit(1)
|> select([a], a.id)
|> Repo.one()
end end
def fetch_public_activities(opts \\ %{}) do def fetch_public_activities(opts \\ %{}) do
@ -514,8 +549,6 @@ defp restrict_visibility(query, %{visibility: visibility})
) )
) )
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
query query
else else
Logger.error("Could not restrict visibility to #{visibility}") Logger.error("Could not restrict visibility to #{visibility}")
@ -531,8 +564,6 @@ defp restrict_visibility(query, %{visibility: visibility})
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility) fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
) )
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
query query
end end
@ -543,6 +574,18 @@ defp restrict_visibility(_query, %{visibility: visibility})
defp restrict_visibility(query, _visibility), do: query 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)
)
query
end
defp restrict_thread_visibility(query, _), do: query
def fetch_user_activities(user, reading_user, params \\ %{}) do def fetch_user_activities(user, reading_user, params \\ %{}) do
params = params =
params params
@ -619,20 +662,6 @@ defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
defp restrict_tag(query, _), do: query 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, [], _user), do: query
defp restrict_recipients(query, recipients, nil) do defp restrict_recipients(query, recipients, nil) do
@ -669,6 +698,12 @@ defp restrict_type(query, %{"type" => type}) do
defp restrict_type(query, _), do: query defp restrict_type(query, _), do: query
defp restrict_state(query, %{"state" => state}) do
from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state))
end
defp restrict_state(query, _), do: query
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
from( from(
activity in query, activity in query,
@ -724,8 +759,11 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info.blocks || [] blocks = info.blocks || []
domain_blocks = info.domain_blocks || [] domain_blocks = info.domain_blocks || []
query =
if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
from( from(
activity in query, [activity, object: o] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks), where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (? && ?)", activity.recipients, ^blocks), where: fragment("not (? && ?)", activity.recipients, ^blocks),
where: where:
@ -735,7 +773,8 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
activity.data, activity.data,
^blocks ^blocks
), ),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks) where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks),
where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks)
) )
end end
@ -776,6 +815,18 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
defp restrict_muted_reblogs(query, _), do: query 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, %{"skip_preload" => true}), do: query
defp maybe_preload_objects(query, _) do defp maybe_preload_objects(query, _) do
@ -783,11 +834,40 @@ defp maybe_preload_objects(query, _) do
|> Activity.with_preloaded_object() |> Activity.with_preloaded_object()
end end
defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query
defp maybe_preload_bookmarks(query, opts) do
query
|> Activity.with_preloaded_bookmark(opts["user"])
end
defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query
defp maybe_set_thread_muted_field(query, opts) do
query
|> Activity.with_set_thread_muted_field(opts["user"])
end
defp maybe_order(query, %{order: :desc}) do
query
|> order_by(desc: :id)
end
defp maybe_order(query, %{order: :asc}) do
query
|> order_by(asc: :id)
end
defp maybe_order(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do def fetch_activities_query(recipients, opts \\ %{}) do
base_query = from(activity in Activity) base_query = from(activity in Activity)
base_query base_query
|> maybe_preload_objects(opts) |> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"]) |> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts) |> restrict_tag(opts)
|> restrict_tag_reject(opts) |> restrict_tag_reject(opts)
@ -796,15 +876,19 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_local(opts) |> restrict_local(opts)
|> restrict_actor(opts) |> restrict_actor(opts)
|> restrict_type(opts) |> restrict_type(opts)
|> restrict_state(opts)
|> restrict_favorited_by(opts) |> restrict_favorited_by(opts)
|> restrict_blocked(opts) |> restrict_blocked(opts)
|> restrict_muted(opts) |> restrict_muted(opts)
|> restrict_media(opts) |> restrict_media(opts)
|> restrict_visibility(opts) |> restrict_visibility(opts)
|> restrict_thread_visibility(opts)
|> restrict_replies(opts) |> restrict_replies(opts)
|> restrict_reblogs(opts) |> restrict_reblogs(opts)
|> restrict_pinned(opts) |> restrict_pinned(opts)
|> restrict_muted_reblogs(opts) |> restrict_muted_reblogs(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
end end
def fetch_activities(recipients, opts \\ %{}) do def fetch_activities(recipients, opts \\ %{}) do
@ -813,9 +897,18 @@ def fetch_activities(recipients, opts \\ %{}) do
|> Enum.reverse() |> Enum.reverse()
end 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) fetch_activities_query([], opts)
|> restrict_to_cc(recipients_to, recipients_cc) |> fetch_activities_bounded_query(recipients, recipients_with_public)
|> Pagination.fetch_paginated(opts) |> Pagination.fetch_paginated(opts)
|> Enum.reverse() |> Enum.reverse()
end end
@ -833,7 +926,7 @@ def upload(file, opts \\ []) do
end end
end end
def user_data_from_user_object(data) do defp object_to_user_data(data) do
avatar = avatar =
data["icon"]["url"] && data["icon"]["url"] &&
%{ %{
@ -880,9 +973,19 @@ def user_data_from_user_object(data) do
{:ok, user_data} {:ok, user_data}
end end
def user_data_from_user_object(data) do
with {:ok, data} <- MRF.filter(data),
{:ok, data} <- object_to_user_data(data) do
{:ok, data}
else
e -> {:error, e}
end
end
def fetch_and_prepare_user_from_ap_id(ap_id) do def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
user_data_from_user_object(data) {:ok, data} <- user_data_from_user_object(data) do
{:ok, data}
else else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}") e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
end end
@ -908,89 +1011,6 @@ def make_user_from_nickname(nickname) do
end end
end end
def should_federate?(inbox, public) do
if public do
true
else
inbox_info = URI.parse(inbox)
!Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
end
def publish(actor, activity) do
remote_followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
public = is_public?(activity)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{source_data: data}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
Federator.publish_single_ap(%{
inbox: inbox,
json: json,
actor: actor,
id: activity.data["id"],
unreachable_since: unreachable_since
})
end)
end
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date =
NaiveDateTime.utc_now()
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
signature =
Pleroma.Web.HTTPSignatures.sign(actor, %{
host: host,
"content-length": byte_size(json),
digest: digest,
date: date
})
with {:ok, %{status: code}} when code in 200..299 <-
result =
@httpoison.post(
inbox,
json,
[
{"Content-Type", "application/activity+json"},
{"Date", date},
{"signature", signature},
{"digest", digest}
]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(inbox)
result
else
{_post_result, response} ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
{:error, response}
end
end
# filter out broken threads # filter out broken threads
def contain_broken_threads(%Activity{} = activity, %User{} = user) do def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user) entire_thread_visible_for_user?(activity, user)
@ -1001,11 +1021,10 @@ def contain_activity(%Activity{} = activity, %User{} = user) do
contain_broken_threads(activity, user) contain_broken_threads(activity, user)
end end
# do post-processing on a timeline def fetch_direct_messages_query do
def contain_timeline(timeline, user) do Activity
timeline |> restrict_type(%{"type" => "Create"})
|> Enum.filter(fn activity -> |> restrict_visibility(%{visibility: "direct"})
contain_activity(activity, user) |> order_by([activity], asc: activity.id)
end)
end end
end end

View file

@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
plug(:relay_active? when action in [:relay]) plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do 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 conn
else else
conn conn
@ -39,7 +39,7 @@ def relay_active?(conn, _) do
def user(conn, %{"nickname" => nickname}) do def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> json(UserView.render("user.json", %{user: user}))
@ -106,7 +106,7 @@ def activity(conn, %{"uuid" => uuid}) do
def following(conn, %{"nickname" => nickname, "page" => page}) do def following(conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
@ -117,7 +117,7 @@ def following(conn, %{"nickname" => nickname, "page" => page}) do
def following(conn, %{"nickname" => nickname}) do def following(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("following.json", %{user: user})) |> json(UserView.render("following.json", %{user: user}))
@ -126,7 +126,7 @@ def following(conn, %{"nickname" => nickname}) do
def followers(conn, %{"nickname" => nickname, "page" => page}) do def followers(conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
@ -137,7 +137,7 @@ def followers(conn, %{"nickname" => nickname, "page" => page}) do
def followers(conn, %{"nickname" => nickname}) do def followers(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("followers.json", %{user: user})) |> json(UserView.render("followers.json", %{user: user}))
@ -146,7 +146,7 @@ def followers(conn, %{"nickname" => nickname}) do
def outbox(conn, %{"nickname" => nickname} = params) do def outbox(conn, %{"nickname" => nickname} = params) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]})) |> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
@ -155,7 +155,7 @@ def outbox(conn, %{"nickname" => nickname} = params) do
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
with %User{} = recipient <- User.get_cached_by_nickname(nickname), with %User{} = recipient <- User.get_cached_by_nickname(nickname),
%User{} = actor <- User.get_or_fetch_by_ap_id(params["actor"]), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
true <- Utils.recipient_in_message(recipient, actor, params), true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params) Federator.incoming_ap_doc(params)
@ -195,7 +195,7 @@ def inbox(conn, params) do
def relay(conn, _params) do def relay(conn, _params) do
with %User{} = user <- Relay.get_actor(), with %User{} = user <- Relay.get_actor(),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> json(UserView.render("user.json", %{user: user}))

View file

@ -17,9 +17,7 @@ def filter(object) do
end end
def get_policies do def get_policies do
Application.get_env(:pleroma, :instance, []) Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies()
|> Keyword.get(:rewrite_policy, [])
|> get_policies()
end end
defp get_policies(policy) when is_atom(policy), do: [policy] defp get_policies(policy) when is_atom(policy), do: [policy]

View file

@ -5,6 +5,8 @@
defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
alias Pleroma.User alias Pleroma.User
@moduledoc "Prevent followbots from following with a bit of heuristic"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
# XXX: this should become User.normalize_by_ap_id() or similar, really. # XXX: this should become User.normalize_by_ap_id() or similar, really.

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do
require Logger require Logger
@moduledoc "Drop and log everything received"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
@impl true @impl true

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
alias Pleroma.Object alias Pleroma.Object
@moduledoc "Ensure a re: is prepended on replies to a post with a Subject"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
@reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless])

View file

@ -4,6 +4,8 @@
defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
alias Pleroma.User alias Pleroma.User
@moduledoc "Block messages with too much mentions (configurable)"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
defp delist_message(message, threshold) when threshold > 0 do defp delist_message(message, threshold) when threshold > 0 do

View file

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@moduledoc "Reject or Word-Replace messages with a keyword or regex"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
defp string_matches?(string, _) when not is_binary(string) do defp string_matches?(string, _) when not is_binary(string) do
false false

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
@moduledoc "Ensure no content placeholder is present (such as the dot from mastodon)"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
@impl true @impl true

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do
@moduledoc "Does nothing (lets the messages go through unmodified)"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
@impl true @impl true

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@moduledoc "Scrub configured hypertext markup"
alias Pleroma.HTML alias Pleroma.HTML
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
alias Pleroma.User alias Pleroma.User
@moduledoc "Rejects non-public (followers-only, direct) activities"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
@impl true @impl true

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
alias Pleroma.User alias Pleroma.User
@moduledoc "Filter activities depending on their origin instance"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
defp check_accept(%{host: actor_host} = _actor_info, object) do defp check_accept(%{host: actor_host} = _actor_info, object) do
@ -47,14 +48,13 @@ defp check_media_nsfw(
%{host: actor_host} = _actor_info, %{host: actor_host} = _actor_info,
%{ %{
"type" => "Create", "type" => "Create",
"object" => %{"attachment" => child_attachment} = child_object "object" => child_object
} = object } = object
) ) do
when length(child_attachment) > 0 do
object = object =
if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_nsfw]), actor_host) do if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_nsfw]), actor_host) do
tags = (child_object["tag"] || []) ++ ["nsfw"] tags = (child_object["tag"] || []) ++ ["nsfw"]
child_object = Map.put(child_object, "tags", tags) child_object = Map.put(child_object, "tag", tags)
child_object = Map.put(child_object, "sensitive", true) child_object = Map.put(child_object, "sensitive", true)
Map.put(object, "object", child_object) Map.put(object, "object", child_object)
else else
@ -74,8 +74,7 @@ defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
actor_host actor_host
), ),
user <- User.get_cached_by_ap_id(object["actor"]), user <- User.get_cached_by_ap_id(object["actor"]),
true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"], true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"] do
true <- user.follower_address in object["cc"] do
to = to =
List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++ List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++
[user.follower_address] [user.follower_address]
@ -94,18 +93,63 @@ defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
{:ok, object} {:ok, object}
end end
defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"} = object) do
if actor_host in Pleroma.Config.get([:mrf_simple, :report_removal]) do
{:reject, nil}
else
{:ok, object}
end
end
defp check_report_removal(_actor_info, object), do: {:ok, object}
defp check_avatar_removal(%{host: actor_host} = _actor_info, %{"icon" => _icon} = object) do
if actor_host in Pleroma.Config.get([:mrf_simple, :avatar_removal]) do
{:ok, Map.delete(object, "icon")}
else
{:ok, object}
end
end
defp check_avatar_removal(_actor_info, object), do: {:ok, object}
defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image} = object) do
if actor_host in Pleroma.Config.get([:mrf_simple, :banner_removal]) do
{:ok, Map.delete(object, "image")}
else
{:ok, object}
end
end
defp check_banner_removal(_actor_info, object), do: {:ok, object}
@impl true @impl true
def filter(object) do def filter(%{"actor" => actor} = object) do
actor_info = URI.parse(object["actor"]) actor_info = URI.parse(actor)
with {:ok, object} <- check_accept(actor_info, object), with {:ok, object} <- check_accept(actor_info, object),
{:ok, object} <- check_reject(actor_info, object), {:ok, object} <- check_reject(actor_info, object),
{:ok, object} <- check_media_removal(actor_info, object), {:ok, object} <- check_media_removal(actor_info, object),
{:ok, object} <- check_media_nsfw(actor_info, object), {:ok, object} <- check_media_nsfw(actor_info, object),
{:ok, object} <- check_ftl_removal(actor_info, object) do {:ok, object} <- check_ftl_removal(actor_info, object),
{:ok, object} <- check_report_removal(actor_info, object) do
{:ok, object} {:ok, object}
else else
_e -> {:reject, nil} _e -> {:reject, nil}
end end
end end
def filter(%{"id" => actor, "type" => obj_type} = object)
when obj_type in ["Application", "Group", "Organization", "Person", "Service"] do
actor_info = URI.parse(actor)
with {:ok, object} <- check_avatar_removal(actor_info, object),
{:ok, object} <- check_banner_removal(actor_info, object) do
{:ok, object}
else
_e -> {:reject, nil}
end
end
def filter(object), do: {:ok, object}
end end

View file

@ -5,6 +5,19 @@
defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
alias Pleroma.User alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
@moduledoc """
Apply policies based on user tags
This policy applies policies on a user activities depending on their tags
on your instance.
- `mrf_tag:media-force-nsfw`: Mark as sensitive on presence of attachments
- `mrf_tag:media-strip`: Remove attachments
- `mrf_tag:force-unlisted`: Mark as unlisted (removes from the federated timeline)
- `mrf_tag:sandbox`: Remove from public (local and federated) timelines
- `mrf_tag:disable-remote-subscription`: Reject non-local follow requests
- `mrf_tag:disable-any-subscription`: Reject any follow requests
"""
defp get_tags(%User{tags: tags}) when is_list(tags), do: tags defp get_tags(%User{tags: tags}) when is_list(tags), do: tags
defp get_tags(_), do: [] defp get_tags(_), do: []
@ -18,7 +31,7 @@ defp process_tag(
object = object =
object object
|> Map.put("tags", tags) |> Map.put("tag", tags)
|> Map.put("sensitive", true) |> Map.put("sensitive", true)
message = Map.put(message, "object", object) message = Map.put(message, "object", object)

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
alias Pleroma.Config alias Pleroma.Config
@moduledoc "Accept-list of users from specified instances"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
defp filter_by_list(object, []), do: {:ok, object} defp filter_by_list(object, []), do: {:ok, object}
@ -18,10 +19,12 @@ defp filter_by_list(%{"actor" => actor} = object, allow_list) do
end end
@impl true @impl true
def filter(object) do def filter(%{"actor" => actor} = object) do
actor_info = URI.parse(object["actor"]) actor_info = URI.parse(actor)
allow_list = Config.get([:mrf_user_allowlist, String.to_atom(actor_info.host)], []) allow_list = Config.get([:mrf_user_allowlist, String.to_atom(actor_info.host)], [])
filter_by_list(object, allow_list) filter_by_list(object, allow_list)
end end
def filter(object), do: {:ok, object}
end end

View file

@ -0,0 +1,151 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.HTTP
alias Pleroma.Instances
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Web.ActivityPub.Visibility
@behaviour Pleroma.Web.Federator.Publisher
require Logger
@moduledoc """
ActivityPub outgoing federation module.
"""
@doc """
Determine if an activity can be represented by running it through Transmogrifier.
"""
def is_representable?(%Activity{} = activity) do
with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
true
else
_e ->
false
end
end
@doc """
Publish a single message to a peer. Takes a struct with the following
parameters set:
* `inbox`: the inbox to publish to
* `json`: the JSON message body representing the ActivityPub message
* `actor`: the actor which is signing the message
* `id`: the ActivityStreams URI of the message
"""
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date =
NaiveDateTime.utc_now()
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
signature =
Pleroma.Signature.sign(actor, %{
host: host,
"content-length": byte_size(json),
digest: digest,
date: date
})
with {:ok, %{status: code}} when code in 200..299 <-
result =
HTTP.post(
inbox,
json,
[
{"Content-Type", "application/activity+json"},
{"Date", date},
{"signature", signature},
{"digest", digest}
]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(inbox)
result
else
{_post_result, response} ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
{:error, response}
end
end
defp should_federate?(inbox, public) do
if public do
true
else
inbox_info = URI.parse(inbox)
!Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
end
@doc """
Publishes an activity to all relevant peers.
"""
def publish(%User{} = actor, %Activity{} = activity) do
remote_followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
public = is_public?(activity)
if public && Config.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
end
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{source_data: data}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
Pleroma.Web.Federator.Publisher.enqueue_one(
__MODULE__,
%{
inbox: inbox,
json: json,
actor: actor,
id: activity.data["id"],
unreachable_since: unreachable_since
}
)
end)
end
def gather_webfinger_links(%User{} = user) do
[
%{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
%{
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href" => user.ap_id
}
]
end
def gather_nodeinfo_protocol_names, do: ["activitypub"]
end

View file

@ -15,7 +15,7 @@ def get_actor do
def follow(target_instance) do def follow(target_instance) do
with %User{} = local_user <- get_actor(), with %User{} = local_user <- get_actor(),
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance), {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.follow(local_user, target_user) do {:ok, activity} <- ActivityPub.follow(local_user, target_user) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}") Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity} {:ok, activity}
@ -28,7 +28,7 @@ def follow(target_instance) do
def unfollow(target_instance) do def unfollow(target_instance) do
with %User{} = local_user <- get_actor(), with %User{} = local_user <- get_actor(),
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance), {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}") Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity} {:ok, activity}

View file

@ -11,7 +11,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
@ -36,6 +35,7 @@ def fix_object(object) do
|> fix_likes |> fix_likes
|> fix_addressing |> fix_addressing
|> fix_summary |> fix_summary
|> fix_type
end end
def fix_summary(%{"summary" => nil} = object) do def fix_summary(%{"summary" => nil} = object) do
@ -66,7 +66,11 @@ def fix_addressing_list(map, field) do
end end
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 = explicit_to =
to to
|> Enum.filter(fn x -> x in explicit_mentions end) |> Enum.filter(fn x -> x in explicit_mentions end)
@ -77,6 +81,7 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mention
final_cc = final_cc =
(cc ++ explicit_cc) (cc ++ explicit_cc)
|> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
|> Enum.uniq() |> Enum.uniq()
object object
@ -84,7 +89,7 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mention
|> Map.put("cc", final_cc) |> Map.put("cc", final_cc)
end 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 # if directMessage flag is set to true, leave the addressing alone
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
@ -94,10 +99,12 @@ def fix_explicit_addressing(object) do
object object
|> Utils.determine_explicit_mentions() |> 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 explicit_mentions =
|> fix_explicit_addressing(explicit_mentions) explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection]
fix_explicit_addressing(object, explicit_mentions, follower_collection)
end end
# if as:Public is addressed, then make sure the followers collection is also addressed # if as:Public is addressed, then make sure the followers collection is also addressed
@ -126,7 +133,7 @@ def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collec
def fix_implicit_addressing(object, _), do: object def fix_implicit_addressing(object, _), do: object
def fix_addressing(object) do def fix_addressing(object) do
%User{} = user = User.get_or_fetch_by_ap_id(object["actor"]) {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
followers_collection = User.ap_followers(user) followers_collection = User.ap_followers(user)
object object
@ -134,7 +141,7 @@ def fix_addressing(object) do
|> fix_addressing_list("cc") |> fix_addressing_list("cc")
|> fix_addressing_list("bto") |> fix_addressing_list("bto")
|> fix_addressing_list("bcc") |> fix_addressing_list("bcc")
|> fix_explicit_addressing |> fix_explicit_addressing()
|> fix_implicit_addressing(followers_collection) |> fix_implicit_addressing(followers_collection)
end end
@ -329,6 +336,18 @@ def fix_content_map(%{"contentMap" => content_map} = object) do
def fix_content_map(object), do: object 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.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 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
with true <- id =~ "follows", with true <- id =~ "follows",
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
@ -399,7 +418,7 @@ def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8),
# - tags # - tags
# - emoji # - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) 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) actor = Containment.get_actor(data)
data = data =
@ -407,7 +426,7 @@ def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = obj
|> fix_addressing |> fix_addressing
with nil <- Activity.get_create_by_object_ap_id(object["id"]), with nil <- Activity.get_create_by_object_ap_id(object["id"]),
%User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
object = fix_object(data["object"]) object = fix_object(data["object"])
params = %{ params = %{
@ -436,7 +455,7 @@ def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
) do ) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{:user_blocked, false} <- {:user_blocked, false} <-
@ -485,7 +504,7 @@ def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
) do ) do
with actor <- Containment.get_actor(data), with actor <- Containment.get_actor(data),
%User{} = followed <- User.get_or_fetch_by_ap_id(actor), {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed), {: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(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
@ -511,7 +530,7 @@ def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
) do ) do
with actor <- Containment.get_actor(data), with actor <- Containment.get_actor(data),
%User{} = followed <- User.get_or_fetch_by_ap_id(actor), {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed), {: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(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
@ -535,7 +554,7 @@ def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
) do ) do
with actor <- Containment.get_actor(data), with actor <- Containment.get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id), {:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
{:ok, activity} {:ok, activity}
@ -548,7 +567,7 @@ def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
) do ) do
with actor <- Containment.get_actor(data), with actor <- Containment.get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id), {:ok, object} <- get_obj_helper(object_id),
public <- Visibility.is_public?(data), public <- Visibility.is_public?(data),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
@ -603,7 +622,7 @@ def handle_incoming(
object_id = Utils.get_ap_id(object_id) object_id = Utils.get_ap_id(object_id)
with actor <- Containment.get_actor(data), with actor <- Containment.get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id), {:ok, object} <- get_obj_helper(object_id),
:ok <- Containment.contain_origin(actor.ap_id, object.data), :ok <- Containment.contain_origin(actor.ap_id, object.data),
{:ok, activity} <- ActivityPub.delete(object, false) do {:ok, activity} <- ActivityPub.delete(object, false) do
@ -622,7 +641,7 @@ def handle_incoming(
} = data } = data
) do ) do
with actor <- Containment.get_actor(data), with actor <- Containment.get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id), {:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity} {:ok, activity}
@ -640,7 +659,7 @@ def handle_incoming(
} = _data } = _data
) do ) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
User.unfollow(follower, followed) User.unfollow(follower, followed)
{:ok, activity} {:ok, activity}
@ -659,7 +678,7 @@ def handle_incoming(
) do ) do
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]), with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked <- User.get_cached_by_ap_id(blocked), %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
%User{} = blocker <- User.get_or_fetch_by_ap_id(blocker), {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
User.unblock(blocker, blocked) User.unblock(blocker, blocked)
{:ok, activity} {:ok, activity}
@ -673,7 +692,7 @@ def handle_incoming(
) do ) do
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]), with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked), %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
%User{} = blocker = User.get_or_fetch_by_ap_id(blocker), {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked) User.unfollow(blocker, blocked)
User.block(blocker, blocked) User.block(blocker, blocked)
@ -692,7 +711,7 @@ def handle_incoming(
} = data } = data
) do ) do
with actor <- Containment.get_actor(data), with actor <- Containment.get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id), {:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity} {:ok, activity}
@ -732,6 +751,7 @@ def prepare_object(object) do
|> set_reply_to_uri |> set_reply_to_uri
|> strip_internal_fields |> strip_internal_fields
|> strip_internal_tags |> strip_internal_tags
|> set_type
end end
# @doc # @doc
@ -856,10 +876,16 @@ def add_mention_tags(object) do
|> Map.put("tag", tags ++ mentions) |> Map.put("tag", tags ++ mentions)
end end
def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
user_info = add_emoji_tags(user_info)
object
|> Map.put(:info, user_info)
end
# TODO: we should probably send mtime instead of unix epoch time for updated # TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(object) do def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || [] tags = object["tag"] || []
emoji = object["emoji"] || []
out = out =
emoji emoji
@ -877,6 +903,10 @@ def add_emoji_tags(object) do
|> Map.put("tag", tags ++ out) |> Map.put("tag", tags ++ out)
end end
def add_emoji_tags(object) do
object
end
def set_conversation(object) do def set_conversation(object) do
Map.put(object, "conversation", object["context"]) Map.put(object, "conversation", object["context"])
end end
@ -886,6 +916,12 @@ def set_sensitive(object) do
Map.put(object, "sensitive", "nsfw" in tags) Map.put(object, "sensitive", "nsfw" in tags)
end 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 def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"] attributed_to = object["attributedTo"] || object["actor"]

View file

@ -19,7 +19,9 @@ defmodule Pleroma.Web.ActivityPub.Utils do
require Logger 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)
# Some implementations send the actor URI as the actor field, others send the entire actor object, # Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have. # so figure out what the actor's URI is based on what we have.
@ -670,7 +672,8 @@ def make_flag_data(params, additional) do
"actor" => params.actor.ap_id, "actor" => params.actor.ap_id,
"content" => params.content, "content" => params.content,
"object" => object, "object" => object,
"context" => params.context "context" => params.context,
"state" => "open"
} }
|> Map.merge(additional) |> Map.merge(additional)
end end
@ -682,7 +685,7 @@ def make_flag_data(params, additional) do
""" """
def fetch_ordered_collection(from, pages_left, acc \\ []) do def fetch_ordered_collection(from, pages_left, acc \\ []) do
with {:ok, response} <- Tesla.get(from), with {:ok, response} <- Tesla.get(from),
{:ok, collection} <- Poison.decode(response.body) do {:ok, collection} <- Jason.decode(response.body) do
case collection["type"] do case collection["type"] do
"OrderedCollection" -> "OrderedCollection" ->
# If we've encountered the OrderedCollection and not the page, # If we've encountered the OrderedCollection and not the page,
@ -713,4 +716,94 @@ def fetch_ordered_collection(from, pages_left, acc \\ []) do
end end
end end
end end
#### Report-related helpers
def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
with new_data <- Map.put(activity.data, "state", state),
changeset <- Changeset.change(activity, data: new_data),
{:ok, activity} <- Repo.update(changeset) do
{:ok, activity}
end
end
def update_report_state(_, _), do: {:error, "Unsupported state"}
def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
[to, cc, recipients] =
activity
|> get_updated_targets(visibility)
|> Enum.map(&Enum.uniq/1)
object_data =
activity.object.data
|> Map.put("to", to)
|> Map.put("cc", cc)
{:ok, object} =
activity.object
|> Object.change(%{data: object_data})
|> Object.update_and_set_cache()
activity_data =
activity.data
|> Map.put("to", to)
|> Map.put("cc", cc)
activity
|> Map.put(:object, object)
|> Activity.change(%{data: activity_data, recipients: recipients})
|> Repo.update()
end
def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
defp get_updated_targets(
%Activity{data: %{"to" => to} = data, recipients: recipients},
visibility
) do
cc = Map.get(data, "cc", [])
follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
public = "https://www.w3.org/ns/activitystreams#Public"
case visibility do
"public" ->
to = [public | List.delete(to, follower_address)]
cc = [follower_address | List.delete(cc, public)]
recipients = [public | recipients]
[to, cc, recipients]
"private" ->
to = [follower_address | List.delete(to, public)]
cc = List.delete(cc, public)
recipients = List.delete(recipients, public)
[to, cc, recipients]
"unlisted" ->
to = [follower_address | List.delete(to, public)]
cc = [public | List.delete(cc, follower_address)]
recipients = recipients ++ [follower_address, public]
[to, cc, recipients]
_ ->
[to, cc, recipients]
end
end
def get_existing_votes(actor, %{data: %{"id" => id}}) do
query =
from(
[activity, object: object] in Activity.with_preloaded_object(Activity),
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 end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.UserView do defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Keys
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -12,8 +13,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
import Ecto.Query import Ecto.Query
@ -34,8 +33,8 @@ def render("endpoints.json", _), do: %{}
# the instance itself is not a Person, but instead an Application # the instance itself is not a Person, but instead an Application
def render("user.json", %{user: %{nickname: nil} = user}) do def render("user.json", %{user: %{nickname: nil} = user}) do
{:ok, user} = WebFinger.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys) {:ok, _, public_key} = Keys.keys_from_pem(user.info.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key]) public_key = :public_key.pem_encode([public_key])
@ -62,13 +61,18 @@ def render("user.json", %{user: %{nickname: nil} = user}) do
end end
def render("user.json", %{user: user}) do def render("user.json", %{user: user}) do
{:ok, user} = WebFinger.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys) {:ok, _, public_key} = Keys.keys_from_pem(user.info.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key]) public_key = :public_key.pem_encode([public_key])
endpoints = render("endpoints.json", %{user: user}) endpoints = render("endpoints.json", %{user: user})
user_tags =
user
|> Transmogrifier.add_emoji_tags()
|> Map.get("tag", [])
%{ %{
"id" => user.ap_id, "id" => user.ap_id,
"type" => "Person", "type" => "Person",
@ -87,7 +91,7 @@ def render("user.json", %{user: user}) do
"publicKeyPem" => public_key "publicKeyPem" => public_key
}, },
"endpoints" => endpoints, "endpoints" => endpoints,
"tag" => user.info.source_data["tag"] || [] "tag" => (user.info.source_data["tag"] || []) ++ user_tags
} }
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))

View file

@ -1,6 +1,7 @@
defmodule Pleroma.Web.ActivityPub.Visibility do defmodule Pleroma.Web.ActivityPub.Visibility do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
@ -13,11 +14,12 @@ def is_public?(data) do
end end
def is_private?(activity) do def is_private?(activity) do
unless is_public?(activity) do with false <- is_public?(activity),
follower_address = User.get_cached_by_ap_id(activity.data["actor"]).follower_address %User{follower_address: follower_address} <-
Enum.any?(activity.data["to"], &(&1 == follower_address)) User.get_cached_by_ap_id(activity.data["actor"]) do
follower_address in activity.data["to"]
else else
false _ -> false
end end
end end
@ -38,24 +40,40 @@ def visible_for_user?(activity, user) do
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y)) visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end end
# guard def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
def entire_thread_visible_for_user?(nil, _user), do: false {:ok, %{rows: [[result]]}} =
Ecto.Adapters.SQL.query(Repo, "SELECT thread_visibility($1, $2)", [
user.ap_id,
activity.data["id"]
])
# XXX: Probably even more inefficient than the previous implementation intended to be a placeholder untill https://git.pleroma.social/pleroma/pleroma/merge_requests/971 is in develop result
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength end
def entire_thread_visible_for_user?( def get_visibility(object) do
%Activity{} = tail, public = "https://www.w3.org/ns/activitystreams#Public"
# %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail, to = object.data["to"] || []
user cc = object.data["cc"] || []
) do
case Object.normalize(tail) do
%{data: %{"inReplyTo" => parent_id}} when is_binary(parent_id) ->
parent = Activity.get_in_reply_to_activity(tail)
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
_ -> cond do
visible_for_user?(tail, user) public in to ->
"public"
public in cc ->
"unlisted"
# this should use the sql for the object's activity
Enum.any?(to, &String.contains?(&1, "/followers")) ->
"private"
object.data["directMessage"] == true ->
"direct"
length(cc) > 0 ->
"private"
true ->
"direct"
end end
end end
end end

View file

@ -4,11 +4,16 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIController do defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@ -59,7 +64,7 @@ def user_create(
bio: "." bio: "."
} }
changeset = User.register_changeset(%User{}, user_data, confirmed: true) changeset = User.register_changeset(%User{}, user_data, need_confirmation: false)
{:ok, user} = User.register(changeset) {:ok, user} = User.register(changeset)
conn conn
@ -101,7 +106,10 @@ def list_users(conn, params) do
search_params = %{ search_params = %{
query: params["query"], query: params["query"],
page: page, page: page,
page_size: page_size page_size: page_size,
tags: params["tags"],
name: params["name"],
email: params["email"]
} }
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)), with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
@ -116,11 +124,11 @@ def list_users(conn, params) do
) )
end end
@filters ~w(local external active deactivated) @filters ~w(local external active deactivated is_admin is_moderator)
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{} @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
defp maybe_parse_filters(filters) do defp maybe_parse_filters(filters) do
filters filters
|> String.split(",") |> String.split(",")
@ -284,12 +292,88 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
|> json(token.token) |> json(token.token)
end end
def list_reports(conn, params) do
params =
params
|> Map.put("type", "Flag")
|> Map.put("skip_preload", true)
reports =
[]
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
conn
|> put_view(ReportView)
|> render("index.json", %{reports: reports})
end
def report_show(conn, %{"id" => id}) do
with %Activity{} = report <- Activity.get_by_id(id) do
conn
|> put_view(ReportView)
|> render("show.json", %{report: report})
else
_ -> {:error, :not_found}
end
end
def report_update_state(conn, %{"id" => id, "state" => state}) do
with {:ok, report} <- CommonAPI.update_report_state(id, state) do
conn
|> put_view(ReportView)
|> render("show.json", %{report: report})
end
end
def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
with false <- is_nil(params["status"]),
%Activity{} <- Activity.get_by_id(id) do
params =
params
|> Map.put("in_reply_to_status_id", id)
|> Map.put("visibility", "direct")
{:ok, activity} = CommonAPI.post(user, params)
conn
|> put_view(StatusView)
|> render("status.json", %{activity: activity})
else
true ->
{:param_cast, nil}
nil ->
{:error, :not_found}
end
end
def status_update(conn, %{"id" => id} = params) do
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
conn
|> put_view(StatusView)
|> render("status.json", %{activity: activity})
end
end
def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
end
end
def errors(conn, {:error, :not_found}) do def errors(conn, {:error, :not_found}) do
conn conn
|> put_status(404) |> put_status(404)
|> json("Not found") |> json("Not found")
end end
def errors(conn, {:error, reason}) do
conn
|> put_status(400)
|> json(reason)
end
def errors(conn, {:param_cast, _}) do def errors(conn, {:param_cast, _}) do
conn conn
|> put_status(400) |> put_status(400)

Some files were not shown because too many files have changed in this diff Show more