diff --git a/.buildpacks b/.buildpacks new file mode 100644 index 000000000..31dd57096 --- /dev/null +++ b/.buildpacks @@ -0,0 +1 @@ +https://github.com/hashnuke/heroku-buildpack-elixir diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8b5131dc3..58c9de167 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,8 +52,7 @@ unit-testing: - mix deps.get - mix ecto.create - mix ecto.migrate - - mix test --trace --preload-modules - - mix coveralls + - mix coveralls --trace --preload-modules unit-testing-rum: stage: test @@ -95,3 +94,49 @@ docs-deploy: - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - rsync -hrvz --delete -e "ssh -p ${SSH_PORT}" priv/static/doc/ "${SSH_USER_HOST_LOCATION}/${CI_COMMIT_REF_NAME}" + +review_app: + image: alpine:3.9 + stage: deploy + before_script: + - apk update && apk add openssh-client git + when: manual + environment: + name: review/$CI_COMMIT_REF_NAME + url: https://$CI_ENVIRONMENT_SLUG.pleroma.online/ + on_stop: stop_review_app + only: + - branches + except: + - master + - develop + script: + - echo "$CI_ENVIRONMENT_SLUG" + - mkdir -p ~/.ssh + - eval $(ssh-agent -s) + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - + - ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts + - (ssh -t dokku@pleroma.online -- apps:create "$CI_ENVIRONMENT_SLUG") || true + - ssh -t dokku@pleroma.online -- config:set "$CI_ENVIRONMENT_SLUG" APP_NAME="$CI_ENVIRONMENT_SLUG" APP_HOST="$CI_ENVIRONMENT_SLUG.pleroma.online" MIX_ENV=dokku + - (ssh -t dokku@pleroma.online -- postgres:create $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db) || true + - (ssh -t dokku@pleroma.online -- postgres:link $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db "$CI_ENVIRONMENT_SLUG") || true + - (ssh -t dokku@pleroma.online -- certs:add "$CI_ENVIRONMENT_SLUG" /home/dokku/server.crt /home/dokku/server.key) || true + - git push -f dokku@pleroma.online:$CI_ENVIRONMENT_SLUG $CI_COMMIT_SHA:refs/heads/master + +stop_review_app: + image: alpine:3.9 + stage: deploy + before_script: + - apk update && apk add openssh-client git + when: manual + environment: + name: review/$CI_COMMIT_REF_NAME + action: stop + script: + - echo "$CI_ENVIRONMENT_SLUG" + - mkdir -p ~/.ssh + - eval $(ssh-agent -s) + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - + - ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts + - ssh -t dokku@pleroma.online -- --force apps:destroy "$CI_ENVIRONMENT_SLUG" + - ssh -t dokku@pleroma.online -- --force postgres:destroy $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db diff --git a/CHANGELOG.md b/CHANGELOG.md index b8907a23f..99b42e280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] ### Added +- Add a generic settings store for frontends / clients to use. - Optional SSH access mode. (Needs `erlang-ssh` package on some distributions). - [MongooseIM](https://github.com/esl/MongooseIM) http authentication support. - LDAP authentication @@ -16,7 +17,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mix Tasks: `mix pleroma.database remove_embedded_objects` - Mix Tasks: `mix pleroma.database update_users_following_followers_counts` - Mix Tasks: `mix pleroma.user toggle_confirmed` +- Federation: Support for `Question` and `Answer` objects - Federation: Support for reports +- Configuration: `poll_limits` option - Configuration: `safe_dm_mentions` option - Configuration: `link_name` option - Configuration: `fetch_initial_posts` option @@ -37,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension) - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/) - Mastodon API: `POST /api/v1/accounts` (account creation API) +- Mastodon API: [Polls](https://docs.joinmastodon.org/api/rest/polls/) - ActivityPub C2S: OAuth endpoints - Metadata: RelMe provider - OAuth: added support for refresh tokens @@ -45,6 +49,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - OAuth: added job to clean expired access tokens - MRF: Support for rejecting reports from specific instances (`mrf_simple`) - MRF: Support for stripping avatars and banner images from specific instances (`mrf_simple`) +- MRF: Support for running subchains. - Addressable lists ### Changed @@ -113,11 +118,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON - Mastodon API: Exposing default scope of the user to anyone - Mastodon API: Make `irreversible` field default to `false` [`POST /api/v1/filters`] +- Mastodon API: Replace missing non-nullable Card attributes with empty strings - User-Agent is now sent correctly for all HTTP requests. +- MRF: Simple policy now properly delists imported or relayed statuses ## Removed - Configuration: `config :pleroma, :fe` in favor of the more flexible `config :pleroma, :frontend_configurations` +## [0.9.99999] - 2019-05-31 +### Security +- Mastodon API: Fix lists leaking private posts + ## [0.9.9999] - 2019-04-05 ### Security - Mastodon API: Fix content warnings skipping HTML sanitization diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..7ac187baa --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: mix phx.server +release: mix ecto.migrate diff --git a/config/config.exs b/config/config.exs index e90821d66..7d70c1a5e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -184,9 +184,6 @@ "application/ld+json" => ["activity+json"] } -config :pleroma, :websub, Pleroma.Web.Websub -config :pleroma, :ostatus, Pleroma.Web.OStatus -config :pleroma, :httpoison, Pleroma.HTTP config :tesla, adapter: Tesla.Adapter.Hackney # Configures http settings, upstream proxy etc. @@ -211,6 +208,12 @@ avatar_upload_limit: 2_000_000, background_upload_limit: 4_000_000, banner_upload_limit: 4_000_000, + poll_limits: %{ + max_options: 20, + max_option_chars: 200, + min_expiration: 0, + max_expiration: 365 * 24 * 60 * 60 + }, registrations_open: true, federating: true, federation_reachability_timeout_days: 7, @@ -323,6 +326,8 @@ federated_timeline_removal: [], replace: [] +config :pleroma, :mrf_subchain, match_actor: %{} + config :pleroma, :rich_media, enabled: true config :pleroma, :media_proxy, @@ -456,7 +461,11 @@ config :esshd, enabled: false -oauth_consumer_strategies = String.split(System.get_env("OAUTH_CONSUMER_STRATEGIES") || "") +oauth_consumer_strategies = + System.get_env("OAUTH_CONSUMER_STRATEGIES") + |> to_string() + |> String.split() + |> Enum.map(&hd(String.split(&1, ":"))) ueberauth_providers = for strategy <- oauth_consumer_strategies do diff --git a/config/dokku.exs b/config/dokku.exs new file mode 100644 index 000000000..9ea0ec450 --- /dev/null +++ b/config/dokku.exs @@ -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" diff --git a/config/test.exs b/config/test.exs index 6100989c4..41cddb9bd 100644 --- a/config/test.exs +++ b/config/test.exs @@ -39,8 +39,6 @@ # Reduce hash rounds for testing config :pbkdf2_elixir, rounds: 1 -config :pleroma, :websub, Pleroma.Web.WebsubMock -config :pleroma, :ostatus, Pleroma.Web.OStatusMock config :tesla, adapter: Tesla.Mock config :pleroma, :rich_media, enabled: false diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 946e0e885..27463f8f3 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -43,6 +43,7 @@ Has these additional fields under the `pleroma` object: - `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated - `hide_followers`: boolean, true when the user has follower hiding enabled - `hide_follows`: boolean, true when the user has follow hiding enabled +- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials` ### Source @@ -81,6 +82,14 @@ Additional parameters can be added to the JSON body/Form data: - `hide_favorites` - if true, user's favorites timeline will be hidden - `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API - `default_scope` - the scope returned under `privacy` key in Source subentity +- `pleroma_settings_store` - Opaque user settings to be saved on the backend. + +### Pleroma Settings Store +Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about. + +The parameter should have a form of `{frontend_name: {...}}`, with `frontend_name` identifying your type of client, e.g. `pleroma_fe`. It will overwrite everything under this property, but will not overwrite other frontend's settings. + +This information is returned in the `verify_credentials` endpoint. ## Authentication diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index 4d99a2d2b..edc62727a 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -126,20 +126,6 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi ## `/api/pleroma/admin/`… See [Admin-API](Admin-API.md) -## `/api/v1/pleroma/flavour/:flavour` -* Method `POST` -* Authentication: required -* Response: JSON string. Returns the user flavour or the default one on success, otherwise returns `{"error": "error_msg"}` -* Example response: "glitch" -* Note: This is intended to be used only by mastofe - -## `/api/v1/pleroma/flavour` -* Method `GET` -* Authentication: required -* Response: JSON string. Returns the user flavour or the default one. -* Example response: "glitch" -* Note: This is intended to be used only by mastofe - ## `/api/pleroma/notifications/read` ### Mark a single notification as read * Method `POST` diff --git a/docs/config.md b/docs/config.md index 67b062fe9..718a7912a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -71,6 +71,11 @@ config :pleroma, Pleroma.Emails.Mailer, * `avatar_upload_limit`: File size limit of user’s profile avatars * `background_upload_limit`: File size limit of user’s profile backgrounds * `banner_upload_limit`: File size limit of user’s profile banners +* `poll_limits`: A map with poll limits for **local** polls + * `max_options`: Maximum number of options + * `max_option_chars`: Maximum number of characters per option + * `min_expiration`: Minimum expiration time (in seconds) + * `max_expiration`: Maximum expiration time (in seconds) * `registrations_open`: Enable registrations for anyone, invitations can be enabled when false. * `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`). * `account_activation_required`: Require users to confirm their emails before signing in. @@ -81,6 +86,7 @@ config :pleroma, Pleroma.Emails.Mailer, * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default) * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See ``:mrf_simple`` section) + * `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (see ``:mrf_subchain`` section) * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See ``:mrf_rejectnonpublic`` section) * `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:. * `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. @@ -224,6 +230,21 @@ relates to mascots on the mastodon frontend * `avatar_removal`: List of instances to strip avatars from * `banner_removal`: List of instances to strip banners from +## :mrf_subchain +This policy processes messages through an alternate pipeline when a given message matches certain criteria. +All criteria are configured as a map of regular expressions to lists of policy modules. + +* `match_actor`: Matches a series of regular expressions against the actor field. + +Example: + +``` +config :pleroma, :mrf_subchain, + match_actor: %{ + ~r/https:\/\/example.com/s => [Pleroma.Web.ActivityPub.MRF.DropPolicy] + } +``` + ## :mrf_rejectnonpublic * `allow_followersonly`: whether to allow followers-only posts * `allow_direct`: whether to allow direct messages @@ -492,7 +513,7 @@ Authentication / authorization settings. * `auth_template`: authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.eex`. * `oauth_consumer_template`: OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`. -* `oauth_consumer_strategies`: the list of enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable. +* `oauth_consumer_strategies`: the list of enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable. Each entry in this space-delimited string should be of format `` or `:` (e.g. `twitter` or `keycloak:ueberauth_keycloak_strategy` in case dependency is named differently than `ueberauth_`). ## OAuth consumer mode diff --git a/docs/installation/alpine_linux_en.md b/docs/installation/alpine_linux_en.md index c493816d6..e1d69c873 100644 --- a/docs/installation/alpine_linux_en.md +++ b/docs/installation/alpine_linux_en.md @@ -87,7 +87,7 @@ sudo adduser -S -s /bin/false -h /opt/pleroma -H pleroma ```shell sudo mkdir -p /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma -sudo -Hu pleroma git clone https://git.pleroma.social/pleroma/pleroma /opt/pleroma +sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma ``` * Change to the new directory: diff --git a/docs/installation/arch_linux_en.md b/docs/installation/arch_linux_en.md index 2b040cfbc..26e1ab86a 100644 --- a/docs/installation/arch_linux_en.md +++ b/docs/installation/arch_linux_en.md @@ -66,7 +66,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma ```shell sudo mkdir -p /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma -sudo -Hu pleroma git clone https://git.pleroma.social/pleroma/pleroma /opt/pleroma +sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma ``` * Change to the new directory: diff --git a/docs/installation/centos7_en.md b/docs/installation/centos7_en.md index 76de21ed8..19bff7461 100644 --- a/docs/installation/centos7_en.md +++ b/docs/installation/centos7_en.md @@ -143,7 +143,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma ```shell sudo mkdir -p /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma -sudo -Hu pleroma git clone https://git.pleroma.social/pleroma/pleroma /opt/pleroma +sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma ``` * Change to the new directory: diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md index 9c0ef92d4..7d39ca5f9 100644 --- a/docs/installation/debian_based_en.md +++ b/docs/installation/debian_based_en.md @@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma ```shell sudo mkdir -p /opt/pleroma sudo chown -R pleroma:pleroma /opt/pleroma -sudo -Hu pleroma git clone https://git.pleroma.social/pleroma/pleroma /opt/pleroma +sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma ``` * Change to the new directory: diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md index 41cce6792..84b9666c8 100644 --- a/docs/installation/debian_based_jp.md +++ b/docs/installation/debian_based_jp.md @@ -69,7 +69,7 @@ cd ~ * Gitリポジトリをクローンします。 ``` -git clone https://git.pleroma.social/pleroma/pleroma +git clone -b master https://git.pleroma.social/pleroma/pleroma ``` * 新しいディレクトリに移動します。 diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md index fccaad378..b7c42a477 100644 --- a/docs/installation/gentoo_en.md +++ b/docs/installation/gentoo_en.md @@ -106,7 +106,7 @@ It is highly recommended you use your own fork for the `https://path/to/repo` pa ```shell pleroma$ cd ~ - pleroma$ git clone https://path/to/repo + pleroma$ git clone -b master https://path/to/repo ``` * Change to the new directory: diff --git a/docs/installation/netbsd_en.md b/docs/installation/netbsd_en.md index e0ac98359..a096d5354 100644 --- a/docs/installation/netbsd_en.md +++ b/docs/installation/netbsd_en.md @@ -58,7 +58,7 @@ Clone the repository: ``` $ cd /home/pleroma -$ git clone https://git.pleroma.social/pleroma/pleroma.git +$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git ``` Configure Pleroma. Note that you need a domain name at this point: diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md index 633b08e6c..fcba38b2c 100644 --- a/docs/installation/openbsd_en.md +++ b/docs/installation/openbsd_en.md @@ -29,7 +29,7 @@ This creates a "pleroma" login class and sets higher values than default for dat Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): `useradd -m -L pleroma _pleroma` #### Clone pleroma's directory -Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide. +Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b master https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide. #### Postgresql Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql: diff --git a/docs/installation/openbsd_fi.md b/docs/installation/openbsd_fi.md index fa6faa62d..39819a8c8 100644 --- a/docs/installation/openbsd_fi.md +++ b/docs/installation/openbsd_fi.md @@ -44,7 +44,7 @@ Vaihda pleroma-käyttäjään ja mene kotihakemistoosi: Lataa pleroman lähdekoodi: -`$ git clone https://git.pleroma.social/pleroma/pleroma.git` +`$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git` `$ cd pleroma` diff --git a/elixir_buildpack.config b/elixir_buildpack.config new file mode 100644 index 000000000..c23b08fb8 --- /dev/null +++ b/elixir_buildpack.config @@ -0,0 +1,2 @@ +elixir_version=1.8.2 +erlang_version=21.3.7 diff --git a/installation/pleroma-mongooseim.cfg b/installation/pleroma-mongooseim.cfg new file mode 100755 index 000000000..d7567321f --- /dev/null +++ b/installation/pleroma-mongooseim.cfg @@ -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 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: +%%%. diff --git a/installation/pleroma.vcl b/installation/pleroma.vcl index 92153d8ef..154747aa6 100644 --- a/installation/pleroma.vcl +++ b/installation/pleroma.vcl @@ -1,4 +1,4 @@ -vcl 4.0; +vcl 4.1; import std; backend default { @@ -35,24 +35,6 @@ sub vcl_recv { } return(purge); } - - # Pleroma MediaProxy - strip headers that will affect caching - if (req.url ~ "^/proxy/") { - unset req.http.Cookie; - unset req.http.Authorization; - unset req.http.Accept; - return (hash); - } - - # Strip headers that will affect caching from all other static content - # This also permits caching of individual toots and AP Activities - if ((req.url ~ "^/(media|static)/") || - (req.url ~ "(?i)\.(html|js|css|jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|mp4|ogg|webm|svg|swf|ttf|pdf|woff|woff2)$")) - { - unset req.http.Cookie; - unset req.http.Authorization; - return (hash); - } } sub vcl_backend_response { @@ -61,6 +43,12 @@ sub vcl_backend_response { set beresp.do_gzip = true; } + # Retry broken backend responses. + if (beresp.status == 503) { + set bereq.http.X-Varnish-Backend-503 = "1"; + return (retry); + } + # CHUNKED SUPPORT if (bereq.http.x-range ~ "bytes=" && beresp.status == 206) { set beresp.ttl = 10m; @@ -73,8 +61,6 @@ sub vcl_backend_response { return (deliver); } - # Default object caching of 86400s; - set beresp.ttl = 86400s; # Allow serving cached content for 6h in case backend goes down set beresp.grace = 6h; @@ -90,20 +76,6 @@ sub vcl_backend_response { set beresp.ttl = 30s; return (deliver); } - - # Pleroma MediaProxy internally sets headers properly - if (bereq.url ~ "^/proxy/") { - return (deliver); - } - - # Strip cache-restricting headers from Pleroma on static content that we want to cache - if (bereq.url ~ "(?i)\.(js|css|jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|mp4|ogg|webm|svg|swf|ttf|pdf|woff|woff2)$") - { - unset beresp.http.set-cookie; - unset beresp.http.Cache-Control; - unset beresp.http.x-request-id; - set beresp.http.Cache-Control = "public, max-age=86400"; - } } # The synthetic response for 301 redirects @@ -132,10 +104,32 @@ sub vcl_hash { } sub vcl_backend_fetch { + # Be more lenient for slow servers on the fediverse + if bereq.url ~ "^/proxy/" { + set bereq.first_byte_timeout = 300s; + } + # CHUNKED SUPPORT if (bereq.http.x-range) { set bereq.http.Range = bereq.http.x-range; } + + if (bereq.retries == 0) { + # Clean up the X-Varnish-Backend-503 flag that is used internally + # to mark broken backend responses that should be retried. + unset bereq.http.X-Varnish-Backend-503; + } else { + if (bereq.http.X-Varnish-Backend-503) { + if (bereq.method != "POST" && + std.healthy(bereq.backend) && + bereq.retries <= 4) { + # Flush broken backend response flag & try again. + unset bereq.http.X-Varnish-Backend-503; + } else { + return (abandon); + } + } + } } sub vcl_deliver { @@ -145,3 +139,9 @@ sub vcl_deliver { unset resp.http.CR; } } + +sub vcl_backend_error { + # Retry broken backend responses. + set bereq.http.X-Varnish-Backend-503 = "1"; + return (retry); +} diff --git a/lib/healthcheck.ex b/lib/healthcheck.ex index 646fb3b9d..32aafc210 100644 --- a/lib/healthcheck.ex +++ b/lib/healthcheck.ex @@ -29,13 +29,13 @@ def system_info do end defp assign_db_info(healthcheck) do - database = Application.get_env(:pleroma, Repo)[:database] + database = Pleroma.Config.get([Repo, :database]) query = "select state, count(pid) from pg_stat_activity where datname = '#{database}' group by state;" result = Repo.query!(query) - pool_size = Application.get_env(:pleroma, Repo)[:pool_size] + pool_size = Pleroma.Config.get([Repo, :pool_size]) db_info = Enum.reduce(result.rows, %{active: 0, idle: 0}, fn [state, cnt], states -> diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex index 238c1acf2..bc97b39ca 100644 --- a/lib/pleroma/conversation.ex +++ b/lib/pleroma/conversation.ex @@ -49,7 +49,7 @@ def create_or_bump_for(activity, opts \\ []) do with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity), "Create" <- activity.data["type"], object <- Pleroma.Object.normalize(activity), - "Note" <- object.data["type"], + true <- object.data["type"] in ["Note", "Question"], ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do {:ok, conversation} = create_for_ap_id(ap_id) diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index 6390cce4c..7d12eff7f 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -22,7 +22,7 @@ defmodule Pleroma.Emoji do @ets __MODULE__.Ets @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 def start_link do @@ -112,7 +112,7 @@ defp load do # Compat thing for old custom emoji handling & default emoji, # it should run even if there are no emoji packs - shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || [] + shortcode_globs = Pleroma.Config.get([:emoji, :shortcode_globs], []) emojis = (load_from_file("config/emoji.txt") ++ diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 3e3b9fe97..607843a5b 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -8,7 +8,7 @@ defmodule Pleroma.Formatter do alias Pleroma.User alias Pleroma.Web.MediaProxy - @safe_mention_regex ~r/^(\s*(?@.+?\s+)+)(?.*)/s + @safe_mention_regex ~r/^(\s*(?(@.+?\s+){1,})+)(?.*)/s @link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/ diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index d1da746de..e5e78ee4f 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -104,7 +104,6 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do paragraphs, breaks and links are allowed through the filter. """ - @markup Application.get_env(:pleroma, :markup) @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) require HtmlSanitizeEx.Scrubber.Meta @@ -142,9 +141,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do Meta.allow_tag_with_these_attributes("span", []) # allow inline images for custom emoji - @allow_inline_images Keyword.get(@markup, :allow_inline_images) - - if @allow_inline_images do + if Pleroma.Config.get([:markup, :allow_inline_images]) do # restrict img tags to http/https only, because of MediaProxy. Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"]) @@ -168,7 +165,6 @@ defmodule Pleroma.HTML.Scrubber.Default do # credo:disable-for-previous-line # No idea how to fix this one… - @markup Application.get_env(:pleroma, :markup) @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) Meta.remove_cdata_sections_before_scrub() @@ -213,7 +209,7 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"]) Meta.allow_tag_with_these_attributes("span", []) - @allow_inline_images Keyword.get(@markup, :allow_inline_images) + @allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images]) if @allow_inline_images do # restrict img tags to http/https only, because of MediaProxy. @@ -228,9 +224,7 @@ defmodule Pleroma.HTML.Scrubber.Default do ]) end - @allow_tables Keyword.get(@markup, :allow_tables) - - if @allow_tables do + if Pleroma.Config.get([:markup, :allow_tables]) do Meta.allow_tag_with_these_attributes("table", []) Meta.allow_tag_with_these_attributes("tbody", []) Meta.allow_tag_with_these_attributes("td", []) @@ -239,9 +233,7 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes("tr", []) end - @allow_headings Keyword.get(@markup, :allow_headings) - - if @allow_headings do + if Pleroma.Config.get([:markup, :allow_headings]) do Meta.allow_tag_with_these_attributes("h1", []) Meta.allow_tag_with_these_attributes("h2", []) Meta.allow_tag_with_these_attributes("h3", []) @@ -249,9 +241,7 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes("h5", []) end - @allow_fonts Keyword.get(@markup, :allow_fonts) - - if @allow_fonts do + if Pleroma.Config.get([:markup, :allow_fonts]) do Meta.allow_tag_with_these_attributes("font", ["face"]) end diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index 558005c19..c216cdcb1 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -32,9 +32,11 @@ def new(opts \\ []) do defp hackney_options(opts) do options = Keyword.get(opts, :adapter, []) adapter_options = Pleroma.Config.get([:http, :adapter], []) + proxy_url = Pleroma.Config.get([:http, :proxy_url], nil) @hackney_options |> Keyword.merge(adapter_options) |> Keyword.merge(options) + |> Keyword.merge(proxy: proxy_url) end end diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index c5f720bc9..c96ee7353 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -65,12 +65,9 @@ defp process_sni_options(options, url) do end def process_request_options(options) do - config = Application.get_env(:pleroma, :http, []) - proxy = Keyword.get(config, :proxy_url, nil) - - case proxy do + case Pleroma.Config.get([:http, :proxy_url]) do nil -> options - _ -> options ++ [proxy: proxy] + proxy -> options ++ [proxy: proxy] end end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 844264307..46f2107b1 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -127,10 +127,15 @@ def dismiss(%{id: user_id} = _user, id) do def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity) 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) - {:ok, notifications} + unless object && object.data["type"] == "Answer" do + users = get_notified_from_activity(activity) + notifications = Enum.map(users, fn user -> create_notification(activity, user) end) + {:ok, notifications} + else + {:ok, []} + end end def create_notifications(_), do: {:ok, []} @@ -166,7 +171,16 @@ def get_notified_from_activity( def get_notified_from_activity(_, _local_only), do: [] def skip?(activity, user) do - [:self, :blocked, :local, :muted, :followers, :follows, :recently_followed] + [ + :self, + :blocked, + :muted, + :followers, + :follows, + :non_followers, + :non_follows, + :recently_followed + ] |> Enum.any?(&skip?(&1, activity, user)) end @@ -179,12 +193,6 @@ def skip?(:blocked, activity, user) do User.blocks?(user, %{ap_id: actor}) end - def skip?(:local, %{local: true}, %{info: %{notification_settings: %{"local" => false}}}), - do: true - - def skip?(:local, %{local: false}, %{info: %{notification_settings: %{"remote" => false}}}), - do: true - def skip?(:muted, activity, user) do actor = activity.data["actor"] @@ -201,12 +209,32 @@ def skip?( User.following?(follower, user) end + def skip?( + :non_followers, + activity, + %{info: %{notification_settings: %{"non_followers" => false}}} = user + ) do + actor = activity.data["actor"] + follower = User.get_cached_by_ap_id(actor) + !User.following?(follower, user) + end + def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do actor = activity.data["actor"] followed = User.get_cached_by_ap_id(actor) User.following?(user, followed) end + def skip?( + :non_follows, + activity, + %{info: %{notification_settings: %{"non_follows" => false}}} = user + ) do + actor = activity.data["actor"] + followed = User.get_cached_by_ap_id(actor) + !User.following?(user, followed) + end + def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do actor = activity.data["actor"] diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index cc6fc9c5d..4b181ec59 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -35,6 +35,9 @@ def change(struct, params \\ %{}) do |> unique_constraint(:ap_id, name: :objects_unique_apid_index) end + def get_by_id(nil), do: nil + def get_by_id(id), do: Repo.get(Object, id) + def get_by_ap_id(nil), do: nil def get_by_ap_id(ap_id) do @@ -195,4 +198,34 @@ def decrease_replies_count(ap_id) do _ -> {:error, "Not found"} end end + + def increase_vote_count(ap_id, name) do + with %Object{} = object <- Object.normalize(ap_id), + "Question" <- object.data["type"] do + multiple = Map.has_key?(object.data, "anyOf") + + options = + (object.data["anyOf"] || object.data["oneOf"] || []) + |> Enum.map(fn + %{"name" => ^name} = option -> + Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1)) + + option -> + option + end) + + data = + if multiple do + Map.put(object.data, "anyOf", options) + else + Map.put(object.data, "oneOf", options) + end + + object + |> Object.change(%{data: data}) + |> update_and_set_cache() + else + _ -> :noop + end + end end diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index bb9388d4f..ca980c629 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -1,4 +1,5 @@ defmodule Pleroma.Object.Fetcher do + alias Pleroma.HTTP alias Pleroma.Object alias Pleroma.Object.Containment alias Pleroma.Web.ActivityPub.Transmogrifier @@ -6,8 +7,6 @@ defmodule Pleroma.Object.Fetcher do require Logger - @httpoison Application.get_env(:pleroma, :httpoison) - defp reinject_object(data) do Logger.debug("Reinjecting object #{data["id"]}") @@ -78,7 +77,7 @@ def fetch_and_contain_remote_object_from_id(id) do with true <- String.starts_with?(id, "http"), {:ok, %{body: body, status: code}} when code in 200..299 <- - @httpoison.get( + HTTP.get( id, [{:Accept, "application/activity+json"}] ), diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex index effc154bf..4dc4e9279 100644 --- a/lib/pleroma/plugs/federating_plug.ex +++ b/lib/pleroma/plugs/federating_plug.ex @@ -10,7 +10,7 @@ def init(options) do end def call(conn, _opts) do - if Keyword.get(Application.get_env(:pleroma, :instance), :federating) do + if Pleroma.Config.get([:instance, :federating]) do conn else conn diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index a3f177fec..285d57309 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -3,6 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.ReverseProxy do + alias Pleroma.HTTP + @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++ ~w(if-unmodified-since if-none-match if-range range) @resp_cache_headers ~w(etag date last-modified cache-control) @@ -59,9 +61,6 @@ defmodule Pleroma.ReverseProxy do * `http`: options for [hackney](https://github.com/benoitc/hackney). """ - @hackney Application.get_env(:pleroma, :hackney, :hackney) - @httpoison Application.get_env(:pleroma, :httpoison, HTTPoison) - @default_hackney_options [] @inline_content_types [ @@ -97,7 +96,7 @@ def call(conn = %{method: method}, url, opts) when method in @methods do hackney_opts = @default_hackney_options |> Keyword.merge(Keyword.get(opts, :http, [])) - |> @httpoison.process_request_options() + |> HTTP.process_request_options() req_headers = build_req_headers(conn.req_headers, opts) @@ -147,7 +146,7 @@ defp request(method, url, headers, hackney_opts) do Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}") method = method |> String.downcase() |> String.to_existing_atom() - case @hackney.request(method, url, headers, "", hackney_opts) do + case :hackney.request(method, url, headers, "", hackney_opts) do {:ok, code, headers, client} when code in @valid_resp_codes -> {:ok, code, downcase_headers(headers), client} @@ -197,7 +196,7 @@ defp chunk_reply(conn, client, opts, sent_so_far, duration) do duration, Keyword.get(opts, :max_read_duration, @max_read_duration) ), - {:ok, data} <- @hackney.stream_body(client), + {:ok, data} <- :hackney.stream_body(client), {:ok, duration} <- increase_read_duration(duration), sent_so_far = sent_so_far + byte_size(data), :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)), diff --git a/lib/pleroma/uploaders/mdii.ex b/lib/pleroma/uploaders/mdii.ex index 190ed9f3a..237544337 100644 --- a/lib/pleroma/uploaders/mdii.ex +++ b/lib/pleroma/uploaders/mdii.ex @@ -4,11 +4,10 @@ defmodule Pleroma.Uploaders.MDII do alias Pleroma.Config + alias Pleroma.HTTP @behaviour Pleroma.Uploaders.Uploader - @httpoison Application.get_env(:pleroma, :httpoison) - # MDII-hosted images are never passed through the MediaPlug; only local media. # Delegate to Pleroma.Uploaders.Local def get_file(file) do @@ -25,7 +24,7 @@ def put_file(upload) do query = "#{cgi}?#{extension}" with {:ok, %{status: 200, body: body}} <- - @httpoison.post(query, file_data, [], adapter: [pool: :default]) do + HTTP.post(query, file_data, [], adapter: [pool: :default]) do remote_file_name = String.split(body) |> List.first() public_url = "#{files}/#{remote_file_name}.#{extension}" {:ok, {:url, public_url}} diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 653dec95f..474cd8c1a 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -366,9 +366,7 @@ def follow_all(follower, followeds) do end def follow(%User{} = follower, %User{info: info} = followed) do - user_config = Application.get_env(:pleroma, :user) - deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked) - + deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) ap_followers = followed.follower_address cond do @@ -760,7 +758,7 @@ def search_query(query, for_user) do from(s in subquery(boost_search_rank_query(distinct_query, for_user)), order_by: [desc: s.search_rank], - limit: 20 + limit: 40 ) end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 6397e2737..fb9ab92ab 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -42,12 +42,17 @@ defmodule Pleroma.User.Info do field(:hide_follows, :boolean, default: false) field(:hide_favorites, :boolean, default: true) field(:pinned_activities, {:array, :string}, default: []) - field(:flavour, :string, default: nil) field(:mascot, :map, default: nil) field(:emoji, {:array, :map}, default: []) + field(:pleroma_settings_store, :map, default: %{}) field(:notification_settings, :map, - default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true} + default: %{ + "followers" => true, + "follows" => true, + "non_follows" => true, + "non_followers" => true + } ) # Found in the wild @@ -68,10 +73,15 @@ def set_activation_status(info, deactivated) do end def update_notification_settings(info, settings) do + settings = + settings + |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end) + |> Map.new() + notification_settings = info.notification_settings |> Map.merge(settings) - |> Map.take(["remote", "local", "followers", "follows"]) + |> Map.take(["followers", "follows", "non_follows", "non_followers"]) params = %{notification_settings: notification_settings} @@ -209,7 +219,8 @@ def profile_update(info, params) do :hide_followers, :hide_favorites, :background, - :show_role + :show_role, + :pleroma_settings_store ]) end @@ -241,14 +252,6 @@ def mastodon_settings_update(info, settings) do |> validate_required([:settings]) end - def mastodon_flavour_update(info, flavour) do - params = %{flavour: flavour} - - info - |> cast(params, [:flavour]) - |> validate_required([:flavour]) - end - def mascot_update(info, url) do params = %{mascot: url} diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 48aaabe94..47115aa6e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -107,6 +107,15 @@ def decrease_replies_count_if_reply(%Object{ def decrease_replies_count_if_reply(_object), do: :noop + def increase_poll_votes_if_vote(%{ + "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, + "type" => "Create" + }) do + Object.increase_vote_count(reply_ap_id, name) + end + + def increase_poll_votes_if_vote(_create_data), do: :noop + def insert(map, local \\ true, fake \\ false) when is_map(map) do with nil <- Activity.normalize(map), map <- lazy_put_activity_defaults(map, fake), @@ -182,40 +191,42 @@ def stream_out(activity) do public = "https://www.w3.org/ns/activitystreams#Public" if activity.data["type"] in ["Create", "Announce", "Delete"] do - Pleroma.Web.Streamer.stream("user", activity) - Pleroma.Web.Streamer.stream("list", activity) + object = Object.normalize(activity) + # Do not stream out poll replies + unless object.data["type"] == "Answer" do + Pleroma.Web.Streamer.stream("user", activity) + Pleroma.Web.Streamer.stream("list", activity) - if Enum.member?(activity.data["to"], public) do - Pleroma.Web.Streamer.stream("public", activity) + if Enum.member?(activity.data["to"], public) do + Pleroma.Web.Streamer.stream("public", activity) - if activity.local do - Pleroma.Web.Streamer.stream("public:local", activity) - end + if activity.local do + Pleroma.Web.Streamer.stream("public:local", activity) + end - if activity.data["type"] in ["Create"] do - object = Object.normalize(activity) + if activity.data["type"] in ["Create"] do + object.data + |> Map.get("tag", []) + |> Enum.filter(fn tag -> is_bitstring(tag) end) + |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) - object.data - |> Map.get("tag", []) - |> Enum.filter(fn tag -> is_bitstring(tag) end) - |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) + if object.data["attachment"] != [] do + Pleroma.Web.Streamer.stream("public:media", activity) - if object.data["attachment"] != [] do - Pleroma.Web.Streamer.stream("public:media", activity) - - if activity.local do - Pleroma.Web.Streamer.stream("public:local:media", activity) + if activity.local do + Pleroma.Web.Streamer.stream("public:local:media", activity) + end end end + else + # TODO: Write test, replace with visibility test + if !Enum.member?(activity.data["cc"] || [], public) && + !Enum.member?( + activity.data["to"], + User.get_cached_by_ap_id(activity.data["actor"]).follower_address + ), + do: Pleroma.Web.Streamer.stream("direct", activity) end - else - # TODO: Write test, replace with visibility test - if !Enum.member?(activity.data["cc"] || [], public) && - !Enum.member?( - activity.data["to"], - User.get_cached_by_ap_id(activity.data["actor"]).follower_address - ), - do: Pleroma.Web.Streamer.stream("direct", activity) end end end @@ -234,6 +245,7 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f {:ok, activity} <- insert(create_data, local, fake), {:fake, false, activity} <- {:fake, fake, activity}, _ <- increase_replies_count_if_reply(create_data), + _ <- increase_poll_votes_if_vote(create_data), # Changing note count prior to enqueuing federation task in order to avoid # race conditions on updating user.info {:ok, _actor} <- increase_note_count_if_public(actor, activity), @@ -398,16 +410,12 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru end def block(blocker, blocked, activity_id \\ nil, local \\ true) do - ap_config = Application.get_env(:pleroma, :activitypub) - unfollow_blocked = Keyword.get(ap_config, :unfollow_blocked) - outgoing_blocks = Keyword.get(ap_config, :outgoing_blocks) + outgoing_blocks = Pleroma.Config.get([:activitypub, :outgoing_blocks]) + unfollow_blocked = Pleroma.Config.get([:activitypub, :unfollow_blocked]) - with true <- unfollow_blocked do + if unfollow_blocked do follow_activity = fetch_latest_follow(blocker, blocked) - - if follow_activity do - unfollow(blocker, blocked, nil, local) - end + if follow_activity, do: unfollow(blocker, blocked, nil, local) end with true <- outgoing_blocks, @@ -479,6 +487,7 @@ defp fetch_activities_for_context_query(context, opts) do if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public from(activity in Activity) + |> maybe_preload_objects(opts) |> restrict_blocked(opts) |> restrict_recipients(recipients, opts["user"]) |> where( @@ -491,6 +500,7 @@ defp fetch_activities_for_context_query(context, opts) do ^context ) ) + |> exclude_poll_votes(opts) |> order_by([activity], desc: activity.id) end @@ -498,7 +508,6 @@ defp fetch_activities_for_context_query(context, opts) do def fetch_activities_for_context(context, opts \\ %{}) do context |> fetch_activities_for_context_query(opts) - |> Activity.with_preloaded_object() |> Repo.all() end @@ -506,7 +515,7 @@ def fetch_activities_for_context(context, opts \\ %{}) do Pleroma.FlakeId.t() | nil def fetch_latest_activity_id_for_context(context, opts \\ %{}) do context - |> fetch_activities_for_context_query(opts) + |> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts)) |> limit(1) |> select([a], a.id) |> Repo.one() @@ -652,20 +661,6 @@ defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do defp restrict_tag(query, _), do: query - defp restrict_to_cc(query, recipients_to, recipients_cc) do - from( - activity in query, - where: - fragment( - "(?->'to' \\?| ?) or (?->'cc' \\?| ?)", - activity.data, - ^recipients_to, - activity.data, - ^recipients_cc - ) - ) - end - defp restrict_recipients(query, [], _user), do: query defp restrict_recipients(query, recipients, nil) do @@ -819,6 +814,18 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do defp restrict_muted_reblogs(query, _), do: query + defp exclude_poll_votes(query, %{"include_poll_votes" => "true"}), do: query + + defp exclude_poll_votes(query, _) do + if has_named_binding?(query, :object) do + from([activity, object: o] in query, + where: fragment("not(?->>'type' = ?)", o.data, "Answer") + ) + else + query + end + end + defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query defp maybe_preload_objects(query, _) do @@ -878,6 +885,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_pinned(opts) |> restrict_muted_reblogs(opts) |> Activity.restrict_deactivated_users() + |> exclude_poll_votes(opts) end def fetch_activities(recipients, opts \\ %{}) do @@ -906,9 +914,18 @@ defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) defp maybe_update_cc(activities, _, _), do: activities - def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do + def fetch_activities_bounded_query(query, recipients, recipients_with_public) do + from(activity in query, + where: + fragment("? && ?", activity.recipients, ^recipients) or + (fragment("? && ?", activity.recipients, ^recipients_with_public) and + "https://www.w3.org/ns/activitystreams#Public" in activity.recipients) + ) + end + + def fetch_activities_bounded(recipients, recipients_with_public, opts \\ %{}) do fetch_activities_query([], opts) - |> restrict_to_cc(recipients_to, recipients_cc) + |> fetch_activities_bounded_query(recipients, recipients_with_public) |> Pagination.fetch_paginated(opts) |> Enum.reverse() end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index ad2ca1e54..0182bda46 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do plug(:relay_active? when action in [:relay]) def relay_active?(conn, _) do - if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do + if Pleroma.Config.get([:instance, :allow_relay]) do conn else conn diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index 1aaa20050..10ceef715 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do @callback filter(Map.t()) :: {:ok | :reject, Map.t()} - def filter(object) do - get_policies() + def filter(policies, %{} = object) do + policies |> Enum.reduce({:ok, object}, fn policy, {:ok, object} -> policy.filter(object) @@ -16,10 +16,10 @@ def filter(object) do end) end + def filter(%{} = object), do: get_policies() |> filter(object) + def get_policies do - Application.get_env(:pleroma, :instance, []) - |> Keyword.get(:rewrite_policy, []) - |> get_policies() + Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies() end defp get_policies(policy) when is_atom(policy), do: [policy] diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 890d70a7a..433d23c5f 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -74,8 +74,7 @@ defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do actor_host ), user <- User.get_cached_by_ap_id(object["actor"]), - true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"], - true <- user.follower_address in object["cc"] do + true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"] do to = List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address] diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex new file mode 100644 index 000000000..765704389 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do + alias Pleroma.Config + alias Pleroma.Web.ActivityPub.MRF + + require Logger + + @behaviour MRF + + defp lookup_subchain(actor) do + with matches <- Config.get([:mrf_subchain, :match_actor]), + {match, subchain} <- Enum.find(matches, fn {k, _v} -> String.match?(actor, k) end) do + {:ok, match, subchain} + else + _e -> {:error, :notfound} + end + end + + @impl true + def filter(%{"actor" => actor} = message) do + with {:ok, match, subchain} <- lookup_subchain(actor) do + Logger.debug( + "[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{ + inspect(subchain) + }" + ) + + subchain + |> MRF.filter(message) + else + _e -> {:ok, message} + end + end + + @impl true + def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index fdebdf85c..f376e5618 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do alias Pleroma.Activity alias Pleroma.Config + alias Pleroma.HTTP alias Pleroma.Instances alias Pleroma.User alias Pleroma.Web.ActivityPub.Relay @@ -16,8 +17,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do require Logger - @httpoison Application.get_env(:pleroma, :httpoison) - @moduledoc """ ActivityPub outgoing federation module. """ @@ -63,7 +62,7 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa with {:ok, %{status: code}} when code in 200..299 <- result = - @httpoison.post( + HTTP.post( inbox, json, [ diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 49d1610a7..d22d24479 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -35,6 +35,7 @@ def fix_object(object) do |> fix_likes |> fix_addressing |> fix_summary + |> fix_type end def fix_summary(%{"summary" => nil} = object) do @@ -65,7 +66,11 @@ def fix_addressing_list(map, field) do end end - def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do + def fix_explicit_addressing( + %{"to" => to, "cc" => cc} = object, + explicit_mentions, + follower_collection + ) do explicit_to = to |> Enum.filter(fn x -> x in explicit_mentions end) @@ -76,6 +81,7 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mention final_cc = (cc ++ explicit_cc) + |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end) |> Enum.uniq() object @@ -83,7 +89,7 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mention |> Map.put("cc", final_cc) end - def fix_explicit_addressing(object, _explicit_mentions), do: object + def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object # if directMessage flag is set to true, leave the addressing alone def fix_explicit_addressing(%{"directMessage" => true} = object), do: object @@ -93,10 +99,12 @@ def fix_explicit_addressing(object) do object |> Utils.determine_explicit_mentions() - explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"] + follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address - object - |> fix_explicit_addressing(explicit_mentions) + explicit_mentions = + explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection] + + fix_explicit_addressing(object, explicit_mentions, follower_collection) end # if as:Public is addressed, then make sure the followers collection is also addressed @@ -133,7 +141,7 @@ def fix_addressing(object) do |> fix_addressing_list("cc") |> fix_addressing_list("bto") |> fix_addressing_list("bcc") - |> fix_explicit_addressing + |> fix_explicit_addressing() |> fix_implicit_addressing(followers_collection) end @@ -328,6 +336,18 @@ def fix_content_map(%{"contentMap" => content_map} = object) do def fix_content_map(object), do: object + def fix_type(%{"inReplyTo" => reply_id} = object) when is_binary(reply_id) do + reply = Object.normalize(reply_id) + + if reply.data["type"] == "Question" and object["name"] do + Map.put(object, "type", "Answer") + else + object + end + end + + def fix_type(object), do: object + defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do with true <- id =~ "follows", %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), @@ -398,7 +418,7 @@ def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), # - tags # - emoji def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) - when objtype in ["Article", "Note", "Video", "Page"] do + when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do actor = Containment.get_actor(data) data = @@ -731,6 +751,7 @@ def prepare_object(object) do |> set_reply_to_uri |> strip_internal_fields |> strip_internal_tags + |> set_type end # @doc @@ -898,6 +919,12 @@ def set_sensitive(object) do Map.put(object, "sensitive", "nsfw" in tags) end + def set_type(%{"type" => "Answer"} = object) do + Map.put(object, "type", "Note") + end + + def set_type(object), do: object + def add_attributed_to(object) do attributed_to = object["attributedTo"] || object["actor"] diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index ca8a0844b..b8159e9e5 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do require Logger - @supported_object_types ["Article", "Note", "Video", "Page"] + @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"] @supported_report_states ~w(open closed resolved) @valid_visibilities ~w(public unlisted private direct) @@ -789,4 +789,21 @@ defp get_updated_targets( [to, cc, recipients] end end + + def get_existing_votes(actor, %{data: %{"id" => id}}) do + query = + from( + [activity, object: object] in Activity.with_preloaded_object(Activity), + where: fragment("(?)->>'actor' = ?", activity.data, ^actor), + where: + fragment( + "(?)->>'inReplyTo' = ?", + object.data, + ^to_string(id) + ), + where: fragment("(?)->>'type' = 'Answer'", object.data) + ) + + Repo.all(query) + end end diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 93b50ee47..8965e3253 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -66,6 +66,9 @@ def get_visibility(object) do Enum.any?(to, &String.contains?(&1, "/followers")) -> "private" + object.data["directMessage"] == true -> + "direct" + length(cc) > 0 -> "private" diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index e8199200e..85fb32669 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -119,6 +119,53 @@ def unfavorite(id_or_ap_id, user) do end end + def vote(user, object, choices) do + with "Question" <- object.data["type"], + {:author, false} <- {:author, object.data["actor"] == user.ap_id}, + {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)}, + {options, max_count} <- get_options_and_max_count(object), + option_count <- Enum.count(options), + {:choice_check, {choices, true}} <- + {:choice_check, normalize_and_validate_choice_indices(choices, option_count)}, + {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do + answer_activities = + Enum.map(choices, fn index -> + answer_data = make_answer_data(user, object, Enum.at(options, index)["name"]) + + ActivityPub.create(%{ + to: answer_data["to"], + actor: user, + context: object.data["context"], + object: answer_data, + additional: %{"cc" => answer_data["cc"]} + }) + end) + + object = Object.get_cached_by_ap_id(object.data["id"]) + {:ok, answer_activities, object} + else + {:author, _} -> {:error, "Poll's author can't vote"} + {:existing_votes, _} -> {:error, "Already voted"} + {:choice_check, {_, false}} -> {:error, "Invalid indices"} + {:count_check, false} -> {:error, "Too many choices"} + end + end + + defp get_options_and_max_count(object) do + if Map.has_key?(object.data, "anyOf") do + {object.data["anyOf"], Enum.count(object.data["anyOf"])} + else + {object.data["oneOf"], 1} + end + end + + defp normalize_and_validate_choice_indices(choices, count) do + Enum.map_reduce(choices, true, fn index, valid -> + index = if is_binary(index), do: String.to_integer(index), else: index + {index, if(valid, do: index < count, else: valid)} + end) + end + def get_visibility(%{"visibility" => visibility}, in_reply_to) when visibility in ~w{public unlisted private direct}, do: {visibility, get_replied_to_visibility(in_reply_to)} @@ -159,6 +206,7 @@ def post(user, %{"status" => status} = data) do data, visibility ), + {poll, poll_emoji} <- make_poll_data(data), {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility), bcc <- bcc_for_list(user, visibility), context <- make_context(in_reply_to), @@ -177,13 +225,14 @@ def post(user, %{"status" => status} = data) do tags, cw, cc, - sensitive + sensitive, + poll ), object <- Map.put( object, "emoji", - Formatter.get_emoji_map(full_payload) + Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji) ) do ActivityPub.create( %{ diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index d97a80dd5..9c92c6cea 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -111,6 +111,72 @@ def bcc_for_list(user, {:list, list_id}) do def bcc_for_list(_, _), do: [] + def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) + when is_list(options) do + %{max_expiration: max_expiration, min_expiration: min_expiration} = + limits = Pleroma.Config.get([:instance, :poll_limits]) + + # XXX: There is probably a cleaner way of doing this + try do + # In some cases mastofe sends out strings instead of integers + expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in + + if Enum.count(options) > limits.max_options do + raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options" + end + + {poll, emoji} = + Enum.map_reduce(options, %{}, fn option, emoji -> + if String.length(option) > limits.max_option_chars do + raise ArgumentError, + message: + "Poll options cannot be longer than #{limits.max_option_chars} characters each" + end + + {%{ + "name" => option, + "type" => "Note", + "replies" => %{"type" => "Collection", "totalItems" => 0} + }, Map.merge(emoji, Formatter.get_emoji_map(option))} + end) + + case expires_in do + expires_in when expires_in > max_expiration -> + raise ArgumentError, message: "Expiration date is too far in the future" + + expires_in when expires_in < min_expiration -> + raise ArgumentError, message: "Expiration date is too soon" + + _ -> + :noop + end + + end_time = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(expires_in) + |> NaiveDateTime.to_iso8601() + + poll = + if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do + %{"type" => "Question", "anyOf" => poll, "closed" => end_time} + else + %{"type" => "Question", "oneOf" => poll, "closed" => end_time} + end + + {poll, emoji} + rescue + e in ArgumentError -> e.message + end + end + + def make_poll_data(%{"poll" => poll}) when is_map(poll) do + "Invalid poll" + end + + def make_poll_data(_data) do + {%{}, %{}} + end + def make_content_html( status, attachments, @@ -233,7 +299,8 @@ def make_note_data( tags, cw \\ nil, cc \\ [], - sensitive \\ false + sensitive \\ false, + merge \\ %{} ) do object = %{ "type" => "Note", @@ -248,12 +315,15 @@ def make_note_data( "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq() } - with false <- is_nil(in_reply_to), - %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do - Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) - else - _ -> object - end + object = + with false <- is_nil(in_reply_to), + %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do + Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) + else + _ -> object + end + + Map.merge(object, merge) end def format_naive_asctime(date) do @@ -430,4 +500,15 @@ def conversation_id_to_context(id) do {:error, "No such conversation"} end end + + def make_answer_data(%User{ap_id: ap_id}, object, name) do + %{ + "type" => "Answer", + "actor" => ap_id, + "cc" => [object.data["actor"]], + "to" => [], + "name" => name, + "inReplyTo" => object.data["id"] + } + end end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 9ef30e885..bd76e4295 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -16,17 +16,32 @@ defmodule Pleroma.Web.Endpoint do plug(Pleroma.Plugs.UploadedMedia) + @static_cache_control "public, no-cache" + # InstanceStatic needs to be before Plug.Static to be able to override shipped-static files # If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well - plug(Pleroma.Plugs.InstanceStatic, at: "/") + # Cache-control headers are duplicated in case we turn off etags in the future + plug(Pleroma.Plugs.InstanceStatic, + at: "/", + gzip: true, + cache_control_for_etags: @static_cache_control, + headers: %{ + "cache-control" => @static_cache_control + } + ) plug( Plug.Static, at: "/", from: :pleroma, only: - ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc) + ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc), # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength + gzip: true, + cache_control_for_etags: @static_cache_control, + headers: %{ + "cache-control" => @static_cache_control + } ) plug(Plug.Static.IndexHtml, at: "/pleroma/admin/") @@ -51,7 +66,7 @@ defmodule Pleroma.Web.Endpoint do parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], json_decoder: Jason, - length: Application.get_env(:pleroma, :instance) |> Keyword.get(:upload_limit), + length: Pleroma.Config.get([:instance, :upload_limit]), body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []} ) diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index 6b0b75284..f4c9fe284 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -11,13 +11,11 @@ defmodule Pleroma.Web.Federator do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.Federator.RetryQueue + alias Pleroma.Web.OStatus alias Pleroma.Web.Websub require Logger - @websub Application.get_env(:pleroma, :websub) - @ostatus Application.get_env(:pleroma, :ostatus) - def init do # 1 minute Process.sleep(1000 * 60) @@ -87,12 +85,12 @@ def perform(:verify_websub, websub) do "Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})" end) - @websub.verify(websub) + Websub.verify(websub) end def perform(:incoming_doc, doc) do Logger.info("Got document, trying to parse") - @ostatus.handle_incoming(doc) + OStatus.handle_incoming(doc) end def perform(:incoming_ap_doc, params) do diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 1ec0f30a1..fe2fdcea1 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Conversation.Participation alias Pleroma.Filter alias Pleroma.Formatter + alias Pleroma.HTTP alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Fetcher @@ -55,7 +56,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do when action in [:account_register] ) - @httpoison Application.get_env(:pleroma, :httpoison) @local_mastodon_name "Mastodon-Local" action_fallback(:errors) @@ -124,6 +124,9 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do end) end) |> add_if_present(params, "default_scope", :default_scope) + |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> + {:ok, Map.merge(user.info.pleroma_settings_store, value)} + end) |> add_if_present(params, "header", :banner, fn value -> with %Plug.Upload{} <- value, {:ok, object} <- ActivityPub.upload(value, type: :banner) do @@ -143,7 +146,10 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do CommonAPI.update(user) end - json(conn, AccountView.render("account.json", %{user: user, for: user})) + json( + conn, + AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) + ) else _e -> conn @@ -153,7 +159,9 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do end def verify_credentials(%{assigns: %{user: user}} = conn, _) do - account = AccountView.render("account.json", %{user: user, for: user}) + account = + AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) + json(conn, account) end @@ -197,7 +205,8 @@ def masto_instance(conn, _params) do languages: ["en"], registrations: Pleroma.Config.get([:instance, :registrations_open]), # Extra (not present in Mastodon): - max_toot_chars: Keyword.get(instance, :limit) + max_toot_chars: Keyword.get(instance, :limit), + poll_limits: Keyword.get(instance, :poll_limits) } json(conn, response) @@ -409,6 +418,53 @@ def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do end end + def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Object{} = object <- Object.get_by_id(id), + %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + true <- Visibility.visible_for_user?(activity, user) do + conn + |> put_view(StatusView) + |> try_render("poll.json", %{object: object, for: user}) + else + nil -> + conn + |> put_status(404) + |> json(%{error: "Record not found"}) + + false -> + conn + |> put_status(404) + |> json(%{error: "Record not found"}) + end + end + + def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do + with %Object{} = object <- Object.get_by_id(id), + true <- object.data["type"] == "Question", + %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + true <- Visibility.visible_for_user?(activity, user), + {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do + conn + |> put_view(StatusView) + |> try_render("poll.json", %{object: object, for: user}) + else + nil -> + conn + |> put_status(404) + |> json(%{error: "Record not found"}) + + false -> + conn + |> put_status(404) + |> json(%{error: "Record not found"}) + + {:error, message} -> + conn + |> put_status(422) + |> json(%{error: message}) + end + end + def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do conn @@ -472,12 +528,6 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do params |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) - idempotency_key = - case get_req_header(conn, "idempotency-key") do - [key] -> key - _ -> Ecto.UUID.generate() - end - scheduled_at = params["scheduled_at"] if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do @@ -490,17 +540,40 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do else params = Map.drop(params, ["scheduled_at"]) - {:ok, activity} = - Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> - CommonAPI.post(user, params) - end) + case get_cached_status_or_post(conn, params) do + {:ignore, message} -> + conn + |> put_status(422) + |> json(%{error: message}) - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + {:error, message} -> + conn + |> put_status(422) + |> json(%{error: message}) + + {_, activity} -> + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end end end + defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do + idempotency_key = + case get_req_header(conn, "idempotency-key") do + [key] -> key + _ -> Ecto.UUID.generate() + end + + Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> + case CommonAPI.post(user, params) do + {:ok, activity} -> activity + {:error, message} -> {:ignore, message} + end + end) + end + def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do json(conn, %{}) @@ -1084,7 +1157,7 @@ def status_search(user, query) do from([a, o] in Activity.with_preloaded_object(Activity), where: fragment("?->>'type' = 'Create'", a.data), where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients, - limit: 20 + limit: 40 ) q = @@ -1346,8 +1419,6 @@ def index(%{assigns: %{user: user}} = conn, _params) do accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) - flavour = get_user_flavour(user) - initial_state = %{ meta: %{ @@ -1366,6 +1437,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do max_toot_chars: limit, mascot: User.get_mascot(user)["url"] }, + poll_limits: Config.get([:instance, :poll_limits]), rights: %{ delete_others_notice: present?(user.info.is_moderator), admin: present?(user.info.is_admin) @@ -1433,7 +1505,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do conn |> put_layout(false) |> put_view(MastodonView) - |> render("index.html", %{initial_state: initial_state, flavour: flavour}) + |> render("index.html", %{initial_state: initial_state}) else conn |> put_session(:return_to, conn.request_path) @@ -1456,43 +1528,6 @@ def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _para end end - @supported_flavours ["glitch", "vanilla"] - - def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params) - when flavour in @supported_flavours do - flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour) - - with changeset <- Ecto.Changeset.change(user), - changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng), - {:ok, user} <- User.update_and_set_cache(changeset), - flavour <- user.info.flavour do - json(conn, flavour) - else - e -> - conn - |> put_resp_content_type("application/json") - |> send_resp(500, Jason.encode!(%{"error" => inspect(e)})) - end - end - - def set_flavour(conn, _params) do - conn - |> put_status(400) - |> json(%{error: "Unsupported flavour"}) - end - - def get_flavour(%{assigns: %{user: user}} = conn, _params) do - json(conn, get_user_flavour(user)) - end - - defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do - flavour - end - - defp get_user_flavour(_) do - "glitch" - end - def login(%{assigns: %{user: %User{}}} = conn, _params) do redirect(conn, to: local_mastodon_root_path(conn)) end @@ -1691,7 +1726,7 @@ def suggestions(%{assigns: %{user: user}} = conn, _) do |> String.replace("{{user}}", user) with {:ok, %{status: 200, body: body}} <- - @httpoison.get( + HTTP.get( url, [], adapter: [ diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index b82d3319b..dc32a1525 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -130,6 +130,7 @@ defp do_render("account.json", %{user: user} = opts) do |> maybe_put_role(user, opts[:for]) |> maybe_put_settings(user, opts[:for], user_info) |> maybe_put_notification_settings(user, opts[:for]) + |> maybe_put_settings_store(user, opts[:for], opts) end defp username_from_nickname(string) when is_binary(string) do @@ -152,6 +153,15 @@ defp maybe_put_settings( defp maybe_put_settings(data, _, _, _), do: data + defp maybe_put_settings_store(data, %User{info: info, id: id}, %User{id: id}, %{ + with_pleroma_settings: true + }) do + data + |> Kernel.put_in([:pleroma, :settings_store], info.pleroma_settings_store) + end + + defp maybe_put_settings_store(data, _, _, _), do: data + defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do data |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 8e8f7cf31..af1dcf66d 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -22,9 +22,14 @@ def render("participation.json", %{participation: participation, user: user}) do last_status = StatusView.render("status.json", %{activity: activity, for: user}) + # Conversations return all users except the current user. + users = + participation.conversation.users + |> Enum.reject(&(&1.id == user.id)) + accounts = AccountView.render("accounts.json", %{ - users: participation.conversation.users, + users: users, as: :user }) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index e55f9b96e..6836d331a 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -240,6 +240,7 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity spoiler_text: summary_html, visibility: get_visibility(object), media_attachments: attachments, + poll: render("poll.json", %{object: object, for: opts[:for]}), mentions: mentions, tags: build_tags(tags), application: %{ @@ -290,8 +291,8 @@ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do provider_url: page_url_data.scheme <> "://" <> page_url_data.host, url: page_url, image: image_url |> MediaProxy.url(), - title: rich_media[:title], - description: rich_media[:description], + title: rich_media[:title] || "", + description: rich_media[:description] || "", pleroma: %{ opengraph: rich_media } @@ -329,6 +330,64 @@ def render("attachment.json", %{attachment: attachment}) do } end + def render("poll.json", %{object: object} = opts) do + {multiple, options} = + case object.data do + %{"anyOf" => options} when is_list(options) -> {true, options} + %{"oneOf" => options} when is_list(options) -> {false, options} + _ -> {nil, nil} + end + + if options do + end_time = + (object.data["closed"] || object.data["endTime"]) + |> NaiveDateTime.from_iso8601!() + + expired = + end_time + |> NaiveDateTime.compare(NaiveDateTime.utc_now()) + |> case do + :lt -> true + _ -> false + end + + voted = + if opts[:for] do + existing_votes = + Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object) + + existing_votes != [] or opts[:for].ap_id == object.data["actor"] + else + false + end + + {options, votes_count} = + Enum.map_reduce(options, 0, fn %{"name" => name} = option, count -> + current_count = option["replies"]["totalItems"] || 0 + + {%{ + title: HTML.strip_tags(name), + votes_count: current_count + }, current_count + count} + end) + + %{ + # Mastodon uses separate ids for polls, but an object can't have + # more than one poll embedded so object id is fine + id: object.id, + expires_at: Utils.to_masto_date(end_time), + expired: expired, + multiple: multiple, + votes_count: votes_count, + options: options, + voted: voted, + emojis: build_emojis(object.data["emoji"]) + } + else + nil + end + end + def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do object = Object.normalize(activity) diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 5762e767b..cee6d8481 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -12,25 +12,27 @@ def url(""), do: nil def url("/" <> _ = url), do: url def url(url) do - config = Application.get_env(:pleroma, :media_proxy, []) - domain = URI.parse(url).host - - cond do - !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) -> - url - - Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern -> - String.equivalent?(domain, pattern) - end) -> - url - - true -> - encode_url(url) + if !enabled?() or local?(url) or whitelisted?(url) do + url + else + encode_url(url) end end + defp enabled?, do: Pleroma.Config.get([:media_proxy, :enabled], false) + + defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) + + defp whitelisted?(url) do + %{host: domain} = URI.parse(url) + + Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern -> + String.equivalent?(domain, pattern) + end) + end + def encode_url(url) do - secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] + secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base]) # Must preserve `%2F` for compatibility with S3 # https://git.pleroma.social/pleroma/pleroma/issues/580 @@ -52,7 +54,7 @@ def encode_url(url) do end def decode_url(sig, url) do - secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] + secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base]) sig = Base.url_decode64!(sig, @base64_opts) local_sig = :crypto.hmac(:sha, secret, url) diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 3bf2a0fbc..57f5b61bb 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -12,8 +12,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.Federator.Publisher - plug(Pleroma.Web.FederatingPlug) - def schemas(conn, _params) do response = %{ links: [ @@ -34,20 +32,15 @@ def schemas(conn, _params) do # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field # under software. def raw_nodeinfo do - instance = Application.get_env(:pleroma, :instance) - media_proxy = Application.get_env(:pleroma, :media_proxy) - suggestions = Application.get_env(:pleroma, :suggestions) - chat = Application.get_env(:pleroma, :chat) - gopher = Application.get_env(:pleroma, :gopher) stats = Stats.get_stats() mrf_simple = - Application.get_env(:pleroma, :mrf_simple) + Config.get(:mrf_simple) |> Enum.into(%{}) # This horror is needed to convert regex sigils to strings mrf_keyword = - Application.get_env(:pleroma, :mrf_keyword, []) + Config.get(:mrf_keyword, []) |> Enum.map(fn {key, value} -> {key, Enum.map(value, fn @@ -76,14 +69,7 @@ def raw_nodeinfo do MRF.get_policies() |> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end) - quarantined = Keyword.get(instance, :quarantined_instances) - - quarantined = - if is_list(quarantined) do - quarantined - else - [] - end + quarantined = Config.get([:instance, :quarantined_instances], []) staff_accounts = User.all_superusers() @@ -94,7 +80,7 @@ def raw_nodeinfo do |> Enum.into(%{}, fn {k, v} -> {k, length(v)} end) federation_response = - if Keyword.get(instance, :mrf_transparency) do + if Config.get([:instance, :mrf_transparency]) do %{ mrf_policies: mrf_policies, mrf_simple: mrf_simple, @@ -111,22 +97,23 @@ def raw_nodeinfo do "pleroma_api", "mastodon_api", "mastodon_api_streaming", - if Keyword.get(media_proxy, :enabled) do + "polls", + if Config.get([:media_proxy, :enabled]) do "media_proxy" end, - if Keyword.get(gopher, :enabled) do + if Config.get([:gopher, :enabled]) do "gopher" end, - if Keyword.get(chat, :enabled) do + if Config.get([:chat, :enabled]) do "chat" end, - if Keyword.get(suggestions, :enabled) do + if Config.get([:suggestions, :enabled]) do "suggestions" end, - if Keyword.get(instance, :allow_relay) do + if Config.get([:instance, :allow_relay]) do "relay" end, - if Keyword.get(instance, :safe_dm_mentions) do + if Config.get([:instance, :safe_dm_mentions]) do "safe_dm_mentions" end ] @@ -143,7 +130,7 @@ def raw_nodeinfo do inbound: [], outbound: [] }, - openRegistrations: Keyword.get(instance, :registrations_open), + openRegistrations: Config.get([:instance, :registrations_open]), usage: %{ users: %{ total: stats.user_count || 0 @@ -151,29 +138,30 @@ def raw_nodeinfo do localPosts: stats.status_count || 0 }, metadata: %{ - nodeName: Keyword.get(instance, :name), - nodeDescription: Keyword.get(instance, :description), - private: !Keyword.get(instance, :public, true), + nodeName: Config.get([:instance, :name]), + nodeDescription: Config.get([:instance, :description]), + private: !Config.get([:instance, :public], true), suggestions: %{ - enabled: Keyword.get(suggestions, :enabled, false), - thirdPartyEngine: Keyword.get(suggestions, :third_party_engine, ""), - timeout: Keyword.get(suggestions, :timeout, 5000), - limit: Keyword.get(suggestions, :limit, 23), - web: Keyword.get(suggestions, :web, "") + enabled: Config.get([:suggestions, :enabled], false), + thirdPartyEngine: Config.get([:suggestions, :third_party_engine], ""), + timeout: Config.get([:suggestions, :timeout], 5000), + limit: Config.get([:suggestions, :limit], 23), + web: Config.get([:suggestions, :web], "") }, staffAccounts: staff_accounts, federation: federation_response, - postFormats: Keyword.get(instance, :allowed_post_formats), + pollLimits: Config.get([:instance, :poll_limits]), + postFormats: Config.get([:instance, :allowed_post_formats]), uploadLimits: %{ - general: Keyword.get(instance, :upload_limit), - avatar: Keyword.get(instance, :avatar_upload_limit), - banner: Keyword.get(instance, :banner_upload_limit), - background: Keyword.get(instance, :background_upload_limit) + general: Config.get([:instance, :upload_limit]), + avatar: Config.get([:instance, :avatar_upload_limit]), + banner: Config.get([:instance, :banner_upload_limit]), + background: Config.get([:instance, :background_upload_limit]) }, - accountActivationRequired: Keyword.get(instance, :account_activation_required, false), - invitesEnabled: Keyword.get(instance, :invites_enabled, false), + accountActivationRequired: Config.get([:instance, :account_activation_required], false), + invitesEnabled: Config.get([:instance, :invites_enabled], false), features: features, - restrictedNicknames: Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) + restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]) } } end diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 61515b31e..6ed089d84 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -3,13 +3,12 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.OStatus do - @httpoison Application.get_env(:pleroma, :httpoison) - import Ecto.Query import Pleroma.Web.XML require Logger alias Pleroma.Activity + alias Pleroma.HTTP alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -363,7 +362,7 @@ def get_atom_url(body) do def fetch_activity_from_atom_url(url) do with true <- String.starts_with?(url, "http"), {:ok, %{body: body, status: code}} when code in 200..299 <- - @httpoison.get( + HTTP.get( url, [{:Accept, "application/atom+xml"}] ) do @@ -380,7 +379,7 @@ def fetch_activity_from_html_url(url) do Logger.debug("Trying to fetch #{url}") with true <- String.starts_with?(url, "http"), - {:ok, %{body: body}} <- @httpoison.get(url, []), + {:ok, %{body: body}} <- HTTP.get(url, []), {:ok, atom_url} <- get_atom_url(body) do fetch_activity_from_atom_url(atom_url) else diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 62e8fa610..e4595800c 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -37,7 +37,10 @@ defp parse_url(url) do try do {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options) - html |> maybe_parse() |> clean_parsed_data() |> check_parsed_data() + html + |> maybe_parse() + |> clean_parsed_data() + |> check_parsed_data() rescue e -> {:error, "Parsing error: #{inspect(e)}"} diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 352268b96..e699f6ae2 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -309,8 +309,6 @@ defmodule Pleroma.Web.Router do post("/conversations/:id/read", MastodonAPIController, :conversation_read) get("/endorsements", MastodonAPIController, :empty_array) - - get("/pleroma/flavour", MastodonAPIController, :get_flavour) end scope [] do @@ -335,6 +333,8 @@ defmodule Pleroma.Web.Router do put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) + post("/polls/:id/votes", MastodonAPIController, :poll_vote) + post("/media", MastodonAPIController, :upload) put("/media/:id", MastodonAPIController, :update_media) @@ -350,8 +350,6 @@ defmodule Pleroma.Web.Router do put("/filters/:id", MastodonAPIController, :update_filter) delete("/filters/:id", MastodonAPIController, :delete_filter) - post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour) - get("/pleroma/mascot", MastodonAPIController, :get_mascot) put("/pleroma/mascot", MastodonAPIController, :set_mascot) @@ -426,6 +424,8 @@ defmodule Pleroma.Web.Router do get("/statuses/:id", MastodonAPIController, :get_status) get("/statuses/:id/context", MastodonAPIController, :get_context) + get("/polls/:id", MastodonAPIController, :get_poll) + get("/accounts/:id/statuses", MastodonAPIController, :user_statuses) get("/accounts/:id/followers", MastodonAPIController, :followers) get("/accounts/:id/following", MastodonAPIController, :following) diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index 9fefdbe25..19e3ef401 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -5,11 +5,10 @@ defmodule Pleroma.Web.Salmon do @behaviour Pleroma.Web.Federator.Publisher - @httpoison Application.get_env(:pleroma, :httpoison) - use Bitwise alias Pleroma.Activity + alias Pleroma.HTTP alias Pleroma.Instances alias Pleroma.Keys alias Pleroma.User @@ -153,7 +152,7 @@ def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params), def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do with {:ok, %{status: code}} when code in 200..299 <- - @httpoison.post( + HTTP.post( url, feed, [{"Content-Type", "application/magic-envelope+xml"}] diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex index 3389c91cc..b3cf9ed11 100644 --- a/lib/pleroma/web/templates/layout/app.html.eex +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -4,7 +4,7 @@ - <%= Application.get_env(:pleroma, :instance)[:name] %> + <%= Pleroma.Config.get([:instance, :name]) %>