diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 46fa1c74c..c28468cd4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: elixir:1.6.4 +image: elixir:1.7.2 services: - postgres:9.6.2 diff --git a/CONFIGURATION.md b/CONFIGURATION.md deleted file mode 100644 index 51a76d1b7..000000000 --- a/CONFIGURATION.md +++ /dev/null @@ -1,106 +0,0 @@ -# Configuring Pleroma - -In the `config/` directory, you will find the following relevant files: - -* `config.exs`: default base configuration -* `dev.exs`: default additional configuration for `MIX_ENV=dev` -* `prod.exs`: default additional configuration for `MIX_ENV=prod` - - -Do not modify files in the list above. -Instead, overload the settings by editing the following files: - -* `dev.secret.exs`: custom additional configuration for `MIX_ENV=dev` -* `prod.secret.exs`: custom additional configuration for `MIX_ENV=prod` - -## Uploads configuration - -To configure where to upload files, and wether or not -you want to remove automatically EXIF data from pictures -being uploaded. - - config :pleroma, Pleroma.Upload, - uploads: "uploads", - strip_exif: false - -* `uploads`: where to put the uploaded files, relative to pleroma's main directory. -* `strip_exif`: whether or not to remove EXIF data from uploaded pics automatically. - This needs Imagemagick installed on the system ( apt install imagemagick ). - - -## Block functionality - - config :pleroma, :activitypub, - accept_blocks: true, - unfollow_blocked: true, - outgoing_blocks: true - - config :pleroma, :user, deny_follow_blocked: true - -* `accept_blocks`: whether to accept incoming block activities from - other instances -* `unfollow_blocked`: whether blocks result in people getting - unfollowed -* `outgoing_blocks`: whether to federate blocks to other instances -* `deny_follow_blocked`: whether to disallow following an account that - has blocked the user in question - -## Message Rewrite Filters (MRFs) - -Modify incoming and outgoing posts. - - config :pleroma, :instance, - rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy - -`rewrite_policy` specifies which MRF policies to apply. -It can either be a single policy or a list of policies. -Currently, MRFs availible by default are: - -* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy` -* `Pleroma.Web.ActivityPub.MRF.DropPolicy` -* `Pleroma.Web.ActivityPub.MRF.SimplePolicy` -* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic` - -Some policies, such as SimplePolicy and RejectNonPublic, -can be additionally configured in their respective sections. - -### NoOpPolicy - -Does not modify posts (this is the default `rewrite_policy`) - -### DropPolicy - -Drops all posts. -It generally does not make sense to use this in production. - -### SimplePolicy - -Restricts the visibility of posts from certain instances. - - config :pleroma, :mrf_simple, - media_removal: [], - media_nsfw: [], - federated_timeline_removal: [], - reject: [], - accept: [] - -* `media_removal`: posts from these instances will have attachments - removed -* `media_nsfw`: posts from these instances will have attachments marked - as nsfw -* `federated_timeline_removal`: posts from these instances will be - marked as unlisted -* `reject`: posts from these instances will be dropped -* `accept`: if not empty, only posts from these instances will be accepted - -### RejectNonPublic - -Drops posts with non-public visibility settings. - - config :pleroma :mrf_rejectnonpublic - allow_followersonly: false, - allow_direct: false, - -* `allow_followersonly`: whether to allow follower-only posts through - the filter -* `allow_direct`: whether to allow direct messages through the filter diff --git a/README.md b/README.md index 3523c9a92..c48930960 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ ## About Pleroma -Pleroma is an OStatus-compatible social networking server written in Elixir, compatible with GNU Social and Mastodon. It is high-performance and can run on small devices like a Raspberry Pi. +Pleroma is a microblogging server software that can federate (= exchange messages with) other servers that support the same federation standards (OStatus and ActivityPub). What that means is that you can host a server for yourself or your friends and stay in control of your online identity, but still exchange messages with people on larger servers. Pleroma will federate with all servers that implement either OStatus or ActivityPub, like Friendica, GNU Social, Hubzilla, Mastodon, Misskey, Peertube, and Pixelfed. + +Pleroma is written in Elixir, high-performance and can run on small devices like a Raspberry Pi. For clients it supports both the [GNU Social API with Qvitter extensions](https://twitter-api.readthedocs.io/en/latest/index.html) and the [Mastodon client API](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md). -Mobile clients that are known to work well: +Client applications that are known to work well: * Twidere * Tusky @@ -15,6 +17,7 @@ Mobile clients that are known to work well: * Amaroq (iOS) * Tootdon (Android + iOS) * Tootle (iOS) +* Whalebird (Windows + Mac + Linux) No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org. @@ -36,7 +39,7 @@ While we don't provide docker files, other people have written very good ones. T * Run `mix generate_config`. This will ask you a few questions about your instance and generate a configuration file in `config/generated_config.exs`. Check that and copy it to either `config/dev.secret.exs` or `config/prod.secret.exs`. It will also create a `config/setup_db.psql`; you may want to double-check this file in case you wanted a different username, or database name than the default. Then you need to run the script as PostgreSQL superuser (i.e. `sudo su postgres -c "psql -f config/setup_db.psql"`). It will create a pleroma db user, database and will setup needed extensions that need to be set up. Postgresql super-user privileges are only needed for this step. - * For these next steps, the default will be to run pleroma using the dev configuration file, `config/dev.secret.exs`. To run them using the prod config file, prefix each command at the shell with `MIX_ENV=prod`. For example: `MIX_ENV=prod mix phx.server`. + * For these next steps, the default will be to run pleroma using the dev configuration file, `config/dev.secret.exs`. To run them using the prod config file, prefix each command at the shell with `MIX_ENV=prod`. For example: `MIX_ENV=prod mix phx.server`. Documentation for the config can be found at [``config/config.md``](config/config.md) * Run `mix ecto.migrate` to run the database migrations. You will have to do this again after certain updates. @@ -45,8 +48,6 @@ While we don't provide docker files, other people have written very good ones. T * The common and convenient way for adding HTTPS is by using Nginx as a reverse proxy. You can look at example Nginx configuration in `installation/pleroma.nginx`. If you need TLS/SSL certificates for HTTPS, you can look get some for free with letsencrypt: https://letsencrypt.org/ The simplest way to obtain and install a certificate is to use [Certbot.](https://certbot.eff.org) Depending on your specific setup, certbot may be able to get a certificate and configure your web server automatically. - * [Not tested with system reboot yet!] You'll also want to set up Pleroma to be run as a systemd service. Example .service file can be found in `installation/pleroma.service` you can put it in `/etc/systemd/system/`. - ## Running * By default, it listens on port 4000 (TCP), so you can access it on http://localhost:4000/ (if you are on the same machine). In case of an error it will restart automatically. @@ -55,9 +56,15 @@ While we don't provide docker files, other people have written very good ones. T Pleroma comes with two frontends. The first one, Pleroma FE, can be reached by normally visiting the site. The other one, based on the Mastodon project, can be found by visiting the /web path of your site. ### As systemd service (with provided .service file) +Example .service file can be found in `installation/pleroma.service` you can put it in `/etc/systemd/system/`. Running `service pleroma start` Logs can be watched by using `journalctl -fu pleroma.service` +### As OpenRC service (with provided RC file) +Copy ``installation/init.d/pleroma`` to ``/etc/init.d/pleroma``. +You can add it to the services ran by default with: +``rc-update add pleroma`` + ### Standalone/run by other means Run `mix phx.server` in repository's root, it will output log into stdout/stderr @@ -70,22 +77,6 @@ Add the following to your `dev.secret.exs` or `prod.secret.exs` if you want to p This is useful for running pleroma inside Tor or i2p. -## Admin Tasks - -### Register a User - -Run `mix register_user `. The `name` appears on statuses, while the nickname corresponds to the user, e.g. `@nickname@instance.tld` - -### Password reset - -Run `mix generate_password_reset username` to generate a password reset link that you can then send to the user. - -### Moderators - -You can make users moderators. They will then be able to delete any post. - -Run `mix set_moderator username [true|false]` to make user a moderator or not. - ## Troubleshooting ### No incoming federation diff --git a/config/config.exs b/config/config.exs index 2d2cdda45..9cc558564 100644 --- a/config/config.exs +++ b/config/config.exs @@ -20,7 +20,8 @@ config :pleroma, Pleroma.Uploaders.S3, bucket: nil, - public_endpoint: "https://s3.amazonaws.com" + public_endpoint: "https://s3.amazonaws.com", + force_media_proxy: false config :pleroma, :emoji, shortcode_globs: ["/emoji/custom/**/*.png"] @@ -84,6 +85,9 @@ description: "A Pleroma instance, an alternative fediverse server", limit: 5000, upload_limit: 16_000_000, + avatar_upload_limit: 2_000_000, + background_upload_limit: 4_000_000, + banner_upload_limit: 4_000_000, registrations_open: true, federating: true, allow_relay: true, @@ -172,6 +176,27 @@ limit: 23, web: "https://vinayaka.distsn.org/?{{host}}+{{user}}" +config :pleroma, :http_security, + enabled: true, + sts: false, + sts_max_age: 31_536_000, + ct_max_age: 2_592_000, + referrer_policy: "same-origin" + +config :cors_plug, + max_age: 86_400, + methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"], + expose: [ + "Link", + "X-RateLimit-Reset", + "X-RateLimit-Limit", + "X-RateLimit-Remaining", + "X-Request-Id", + "Idempotency-Key" + ], + credentials: true, + headers: ["Authorization", "Content-Type", "Idempotency-Key"] + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/config/config.md b/config/config.md new file mode 100644 index 000000000..5b4110646 --- /dev/null +++ b/config/config.md @@ -0,0 +1,89 @@ +# Configuration + +This file describe the configuration, it is recommended to edit the relevant *.secret.exs file instead of the others founds in the ``config`` directory. +If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherwise it is ``dev.secret.exs``. + +## Pleroma.Upload +* `uploader`: Select which `Pleroma.Uploaders` to use +* `strip_exif`: boolean, uses ImageMagick(!) to strip exif. + +## Pleroma.Uploaders.Local +* `uploads`: Which directory to store the user-uploads in, relative to pleroma’s working directory +* `uploads_url`: The URL to access a user-uploaded file, ``{{base_url}}`` is replaced to the instance URL and ``{{file}}`` to the filename. Useful when you want to proxy the media files via another host. + +## :uri_schemes +* `valid_schemes`: List of the scheme part that is considered valid to be an URL + +## :instance +* `name`: The instance’s name +* `email`: Email used to reach an Administrator/Moderator of the instance +* `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance`` +* `limit`: Posts character limit (CW/Subject included in the counter) +* `upload_limit`: File size limit of uploads (except for avatar, background, banner) +* `avatar_upload_limit`: File size limit of user’s profile avatars +* `background_upload_limit`: File size limit of user’s profile backgrounds +* `banner_upload_limit`: File size limit of user’s profile backgrounds +* `registerations_open`: Enable registerations for anyone, invitations can be used when false. +* `federating` +* `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance +* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default: + * `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default) + * `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production + * `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See ``:mrf_simple`` section) + * `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See ``:mrf_rejectnonpublic`` section) +* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. +* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. +* `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json`` +* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML) +* `finmoji_enabled`: Whenether to enable the finmojis in the custom emojis. +* `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). + +## :fe +This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false. + +* `theme`: Which theme to use, they are defined in ``styles.json`` +* `logo`: URL of the logo, defaults to Pleroma’s logo +* `logo_mask`: Whenether to mask the logo +* `logo_margin`: What margin to use around the logo +* `background`: URL of the background, unless viewing a user profile with a background that is set +* `redirect_root_no_login`: relative URL which indicates where to redirect when a user isn’t logged in. +* `redirect_root_login`: relative URL which indicates where to redirect when a user is logged in. +* `show_instance_panel`: Whenether to show the instance’s specific panel. +* `scope_options_enabled`: Enable setting an notice visibility and subject/CW when posting +* `formatting_options_enabled`: Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to ``:instance, allowed_post_formats`` +* `collapse_message_with_subjects`: When a message has a subject(aka Content Warning), collapse it by default +* `hide_post_stats`: Hide notices statistics(repeats, favorites, …) +* `hide_user_stats`: Hide profile statistics(posts, posts per day, followers, followings, …) + +## :mrf_simple +* `media_removal`: List of instances to remove medias from +* `media_nsfw`: List of instances to put medias as NSFW(sensitive) from +* `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline +* `reject`: List of instances to reject any activities from +* `accept`: List of instances to accept any activities from + +## :mrf_rejectnonpublic +* `allow_followersonly`: whether to allow followers-only posts +* `allow_direct`: whether to allow direct messages + +## :media_proxy +* `enabled`: Enables proxying of remote media to the instance’s proxy +* `redirect_on_failure`: Use the original URL when Media Proxy fails to get it + +## :gopher +* `enabled`: Enables the gopher interface +* `ip`: IP address to bind to +* `port`: Port to bind to + +## :activitypub +* ``accept_blocks``: Whether to accept incoming block activities from other instances +* ``unfollow_blocked``: Whether blocks result in people getting unfollowed +* ``outgoing_blocks``: Whether to federate blocks to other instances +* ``deny_follow_blocked``: Whether to disallow following an account that has blocked the user in question + +## :http_security +* ``enabled``: Whether the managed content security policy is enabled +* ``sts``: Whether to additionally send a `Strict-Transport-Security` header +* ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent +* ``ct_max_age``: The maximum age for the `Expect-CT` header if sent +* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`. diff --git a/installation/caddyfile-pleroma.example b/installation/caddyfile-pleroma.example index 2c1efde2d..03ff000b6 100644 --- a/installation/caddyfile-pleroma.example +++ b/installation/caddyfile-pleroma.example @@ -1,4 +1,10 @@ -social.domain.tld { +# default Caddyfile config for Pleroma +# +# Simple installation instructions: +# 1. Replace 'example.tld' with your instance's domain wherever it appears. +# 2. Copy this section into your Caddyfile and restart Caddy. + +example.tld { log /var/log/caddy/pleroma_access.log errors /var/log/caddy/pleroma_error.log @@ -9,34 +15,12 @@ social.domain.tld { transparent } - tls user@domain.tld { + tls { # Remove the rest of the lines in here, if you want to support older devices key_type p256 ciphers ECDHE-ECDSA-WITH-CHACHA20-POLY1305 ECDHE-RSA-WITH-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256 } - header / { - X-XSS-Protection "1; mode=block" - X-Frame-Options "DENY" - X-Content-Type-Options "nosniff" - Referrer-Policy "same-origin" - Strict-Transport-Security "max-age=31536000; includeSubDomains;" - Expect-CT "enforce, max-age=2592000" - Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://social.domain.tld; upgrade-insecure-requests;" - } - - # If you do not want remote frontends to be able to access your Pleroma backend server, remove these lines. - # If you want to allow all origins access, remove the origin lines. - # To use this directive, you need the http.cors plugin for Caddy. - cors / { - origin https://halcyon.domain.tld - origin https://pinafore.domain.tld - methods POST,PUT,DELETE,GET,PATCH,OPTIONS - allowed_headers Authorization,Content-Type,Idempotency-Key - exposed_headers Link,X-RateLimit-Reset,X-RateLimit-Limit,X-RateLimit-Remaining,X-Request-Id - } - # Stop removing lines here. - # If you do not want to use the mediaproxy function, remove these lines. # To use this directive, you need the http.cache plugin for Caddy. cache { diff --git a/installation/pleroma-apache.conf b/installation/pleroma-apache.conf index 992c0c900..d5e75044f 100644 --- a/installation/pleroma-apache.conf +++ b/installation/pleroma-apache.conf @@ -1,24 +1,31 @@ -#Example configuration for when Apache httpd and Pleroma are on the same host. -#Needed modules: headers proxy proxy_http proxy_wstunnel rewrite ssl -#This assumes a Debian style Apache config. Put this in /etc/apache2/sites-available -#Install your TLS certificate, possibly using Let's Encrypt. -#Replace 'pleroma.example.com' with your instance's domain wherever it appears +# default Apache site config for Pleroma +# +# needed modules: define headers proxy proxy_http proxy_wstunnel rewrite ssl +# +# Simple installation instructions: +# 1. Install your TLS certificate, possibly using Let's Encrypt. +# 2. Replace 'example.tld' with your instance's domain wherever it appears. +# 3. This assumes a Debian style Apache config. Copy this file to +# /etc/apache2/sites-available/ and then add a symlink to it in +# /etc/apache2/sites-enabled/ by running 'a2ensite pleroma-apache.conf', then restart Apache. -ServerName pleroma.example.com +Define servername example.tld + +ServerName ${servername} ServerTokens Prod ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined - Redirect permanent / https://pleroma.example.com + Redirect permanent / https://${servername} SSLEngine on - SSLCertificateFile /etc/letsencrypt/live/pleroma.example.com/cert.pem - SSLCertificateKeyFile /etc/letsencrypt/live/pleroma.example.com/privkey.pem - SSLCertificateChainFile /etc/letsencrypt/live/pleroma.example.com/fullchain.pem + SSLCertificateFile /etc/letsencrypt/live/${servername}/cert.pem + SSLCertificateKeyFile /etc/letsencrypt/live/${servername}/privkey.pem + SSLCertificateChainFile /etc/letsencrypt/live/${servername}/fullchain.pem # Mozilla modern configuration, tweak to your needs SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 @@ -27,15 +34,6 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined SSLCompression off SSLSessionTickets off - Header always set X-Xss-Protection "1; mode=block" - Header always set X-Frame-Options "DENY" - Header always set X-Content-Type-Options "nosniff" - Header always set Referrer-Policy same-origin - Header always set Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://pleroma.example.tld; upgrade-insecure-requests;" - - # Uncomment this only after you get HTTPS working. - # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" - RewriteEngine On RewriteCond %{HTTP:Connection} Upgrade [NC] RewriteCond %{HTTP:Upgrade} websocket [NC] @@ -45,7 +43,7 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined ProxyPass / http://localhost:4000/ ProxyPassReverse / http://localhost:4000/ - RequestHeader set Host "pleroma.example.com" + RequestHeader set Host ${servername} ProxyPreserveHost On @@ -53,4 +51,4 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined SSLUseStapling on SSLStaplingResponderTimeout 5 SSLStaplingReturnResponderErrors off -SSLStaplingCache shmcb:/var/run/ocsp(128000) \ No newline at end of file +SSLStaplingCache shmcb:/var/run/ocsp(128000) diff --git a/installation/pleroma.nginx b/installation/pleroma.nginx index f648336ca..f0e684f2c 100644 --- a/installation/pleroma.nginx +++ b/installation/pleroma.nginx @@ -10,8 +10,8 @@ proxy_cache_path /tmp/pleroma-media-cache levels=1:2 keys_zone=pleroma_media_cac inactive=720m use_temp_path=off; server { - listen 80; server_name example.tld; + listen 80; return 301 https://$server_name$request_uri; # Uncomment this if you need to use the 'webroot' method with certbot. Make sure @@ -46,7 +46,7 @@ server { ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1; ssl_stapling on; ssl_stapling_verify on; - + server_name example.tld; gzip_vary on; @@ -60,28 +60,6 @@ server { client_max_body_size 16m; location / { - # if you do not want remote frontends to be able to access your Pleroma backend - # server, remove these lines. - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always; - add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always; - if ($request_method = OPTIONS) { - return 204; - } - # stop removing lines here. - - add_header X-XSS-Protection "1; mode=block" always; - add_header X-Permitted-Cross-Domain-Policies "none" always; - add_header X-Frame-Options "DENY" always; - add_header X-Content-Type-Options "nosniff" always; - add_header Referrer-Policy "same-origin" always; - add_header X-Download-Options "noopen" always; - add_header Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action *; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://example.tld; upgrade-insecure-requests;" always; - - # Uncomment this only after you get HTTPS working. - # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; diff --git a/installation/pleroma.service b/installation/pleroma.service index 84747d952..6955e5cc6 100644 --- a/installation/pleroma.service +++ b/installation/pleroma.service @@ -6,6 +6,7 @@ After=network.target postgresql.service User=pleroma WorkingDirectory=/home/pleroma/pleroma Environment="HOME=/home/pleroma" +Environment="MIX_ENV=prod" ExecStart=/usr/local/bin/mix phx.server ExecReload=/bin/kill $MAINPID KillMode=process diff --git a/installation/pleroma.vcl b/installation/pleroma.vcl index 74490be2a..63c1cb74d 100644 --- a/installation/pleroma.vcl +++ b/installation/pleroma.vcl @@ -119,13 +119,3 @@ sub vcl_pipe { set bereq.http.connection = req.http.connection; } } - -sub vcl_deliver { - set resp.http.X-Frame-Options = "DENY"; - set resp.http.X-XSS-Protection = "1; mode=block"; - set resp.http.X-Content-Type-Options = "nosniff"; - set resp.http.Referrer-Policy = "same-origin"; - set resp.http.Content-Security-Policy = "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://" + req.http.host + "; upgrade-insecure-requests;"; - # Uncomment this only after you get HTTPS working. - # set resp.http.Strict-Transport-Security= "max-age=31536000; includeSubDomains"; -} diff --git a/lib/mix/tasks/deactivate_user.ex b/lib/mix/tasks/deactivate_user.ex index 96b3db6e4..e71ed1ec0 100644 --- a/lib/mix/tasks/deactivate_user.ex +++ b/lib/mix/tasks/deactivate_user.ex @@ -2,7 +2,13 @@ defmodule Mix.Tasks.DeactivateUser do use Mix.Task alias Pleroma.User - @shortdoc "Toggle deactivation status for a user" + @moduledoc """ + Deactivates a user (local or remote) + + Usage: ``mix deactivate_user `` + + Example: ``mix deactivate_user lain`` + """ def run([nickname]) do Mix.Task.run("app.start") diff --git a/lib/mix/tasks/generate_config.ex b/lib/mix/tasks/generate_config.ex index 70a110561..e3cbbf131 100644 --- a/lib/mix/tasks/generate_config.ex +++ b/lib/mix/tasks/generate_config.ex @@ -1,7 +1,15 @@ defmodule Mix.Tasks.GenerateConfig do use Mix.Task - @shortdoc "Generates a new config" + @moduledoc """ + Generate a new config + + ## Usage + ``mix generate_config`` + + This mix task is interactive, and will overwrite the config present at ``config/generated_config.exs``. + """ + def run(_) do IO.puts("Answer a few questions to generate a new config\n") IO.puts("--- THIS WILL OVERWRITE YOUR config/generated_config.exs! ---\n") diff --git a/lib/mix/tasks/generate_invite_token.ex b/lib/mix/tasks/generate_invite_token.ex index c4daa9a6c..418ef3790 100644 --- a/lib/mix/tasks/generate_invite_token.ex +++ b/lib/mix/tasks/generate_invite_token.ex @@ -1,7 +1,14 @@ defmodule Mix.Tasks.GenerateInviteToken do use Mix.Task - @shortdoc "Generate invite token for user" + @moduledoc """ + Generates invite token + + This is in the form of a URL to be used by the Invited user to register themselves. + + ## Usage + ``mix generate_invite_token`` + """ def run([]) do Mix.Task.run("app.start") diff --git a/lib/mix/tasks/generate_password_reset.ex b/lib/mix/tasks/generate_password_reset.ex index 6bf640150..f7f4c4f59 100644 --- a/lib/mix/tasks/generate_password_reset.ex +++ b/lib/mix/tasks/generate_password_reset.ex @@ -2,7 +2,13 @@ defmodule Mix.Tasks.GeneratePasswordReset do use Mix.Task alias Pleroma.User - @shortdoc "Generate password reset link for user" + @moduledoc """ + Generate password reset link for user + + Usage: ``mix generate_password_reset `` + + Example: ``mix generate_password_reset lain`` + """ def run([nickname]) do Mix.Task.run("app.start") diff --git a/lib/mix/tasks/make_moderator.ex b/lib/mix/tasks/make_moderator.ex index a454a958e..15586dc30 100644 --- a/lib/mix/tasks/make_moderator.ex +++ b/lib/mix/tasks/make_moderator.ex @@ -1,9 +1,16 @@ defmodule Mix.Tasks.SetModerator do + @moduledoc """ + Set moderator to a local user + + Usage: ``mix set_moderator `` + + Example: ``mix set_moderator lain`` + """ + use Mix.Task import Mix.Ecto alias Pleroma.{Repo, User} - @shortdoc "Set moderator status" def run([nickname | rest]) do Application.ensure_all_started(:pleroma) diff --git a/lib/mix/tasks/reactivate_user.ex b/lib/mix/tasks/reactivate_user.ex new file mode 100644 index 000000000..a30d3ac8b --- /dev/null +++ b/lib/mix/tasks/reactivate_user.ex @@ -0,0 +1,19 @@ +defmodule Mix.Tasks.ReactivateUser do + use Mix.Task + alias Pleroma.User + + @moduledoc """ + Reactivate a user + + Usage: ``mix reactivate_user `` + + Example: ``mix reactivate_user lain`` + """ + def run([nickname]) do + Mix.Task.run("app.start") + + with user <- User.get_by_nickname(nickname) do + User.deactivate(user, false) + end + end +end diff --git a/lib/mix/tasks/register_user.ex b/lib/mix/tasks/register_user.ex index e74721c49..1f5321093 100644 --- a/lib/mix/tasks/register_user.ex +++ b/lib/mix/tasks/register_user.ex @@ -1,4 +1,12 @@ defmodule Mix.Tasks.RegisterUser do + @moduledoc """ + Manually register a local user + + Usage: ``mix register_user `` + + Example: ``mix register_user 仮面の告白 lain lain@example.org "blushy-crushy fediverse idol + pleroma dev" pleaseDontHeckLain`` + """ + use Mix.Task alias Pleroma.{Repo, User} diff --git a/lib/mix/tasks/relay_follow.ex b/lib/mix/tasks/relay_follow.ex index ac6f20924..4d57c6bca 100644 --- a/lib/mix/tasks/relay_follow.ex +++ b/lib/mix/tasks/relay_follow.ex @@ -4,6 +4,13 @@ defmodule Mix.Tasks.RelayFollow do alias Pleroma.Web.ActivityPub.Relay @shortdoc "Follows a remote relay" + @moduledoc """ + Follows a remote relay + + Usage: ``mix relay_follow `` + + Example: ``mix relay_follow https://example.org/relay`` + """ def run([target]) do Mix.Task.run("app.start") diff --git a/lib/mix/tasks/relay_unfollow.ex b/lib/mix/tasks/relay_unfollow.ex index 4621ace83..bd69fd8a0 100644 --- a/lib/mix/tasks/relay_unfollow.ex +++ b/lib/mix/tasks/relay_unfollow.ex @@ -3,7 +3,13 @@ defmodule Mix.Tasks.RelayUnfollow do require Logger alias Pleroma.Web.ActivityPub.Relay - @shortdoc "Follows a remote relay" + @moduledoc """ + Unfollows a remote relay + + Usage: ``mix relay_follow `` + + Example: ``mix relay_follow https://example.org/relay`` + """ def run([target]) do Mix.Task.run("app.start") diff --git a/lib/mix/tasks/rm_user.ex b/lib/mix/tasks/rm_user.ex index 27521b745..50463046c 100644 --- a/lib/mix/tasks/rm_user.ex +++ b/lib/mix/tasks/rm_user.ex @@ -2,12 +2,18 @@ defmodule Mix.Tasks.RmUser do use Mix.Task alias Pleroma.User - @shortdoc "Permanently delete a user" + @moduledoc """ + Permanently deletes a user + + Usage: ``mix rm_user [nickname]`` + + Example: ``mix rm_user lain`` + """ def run([nickname]) do Mix.Task.run("app.start") with %User{local: true} = user <- User.get_by_nickname(nickname) do - User.delete(user) + {:ok, _} = User.delete(user) end end end diff --git a/lib/mix/tasks/sample_config.eex b/lib/mix/tasks/sample_config.eex index 3881ead26..462c34636 100644 --- a/lib/mix/tasks/sample_config.eex +++ b/lib/mix/tasks/sample_config.eex @@ -25,6 +25,10 @@ config :pleroma, Pleroma.Repo, hostname: "localhost", pool_size: 10 +# Enable Strict-Transport-Security once SSL is working: +# config :pleroma, :http_security, +# sts: true + # Configure S3 support if desired. # The public S3 endpoint is different depending on region and provider, # consult your S3 provider's documentation for details on what to use. diff --git a/lib/mix/tasks/sample_psql.eex b/lib/mix/tasks/sample_psql.eex index bc22f166c..b6f57948b 100644 --- a/lib/mix/tasks/sample_psql.eex +++ b/lib/mix/tasks/sample_psql.eex @@ -1,8 +1,5 @@ -CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB; --- in case someone runs this second time accidentally -ALTER USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB; -CREATE DATABASE pleroma_dev; -ALTER DATABASE pleroma_dev OWNER TO pleroma; +CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>'; +CREATE DATABASE pleroma_dev OWNER pleroma; \c pleroma_dev; --Extensions made by ecto.migrate that need superuser access CREATE EXTENSION IF NOT EXISTS citext; diff --git a/lib/mix/tasks/set_locked.ex b/lib/mix/tasks/set_locked.ex index 2b3b18b09..a154595ca 100644 --- a/lib/mix/tasks/set_locked.ex +++ b/lib/mix/tasks/set_locked.ex @@ -1,9 +1,18 @@ defmodule Mix.Tasks.SetLocked do + @moduledoc """ + Lock a local user + + The local user will then have to manually accept/reject followers. This can also be done by the user into their settings. + + Usage: ``mix set_locked `` + + Example: ``mix set_locked lain`` + """ + use Mix.Task import Mix.Ecto alias Pleroma.{Repo, User} - @shortdoc "Set locked status" def run([nickname | rest]) do ensure_started(Repo, []) diff --git a/lib/mix/tasks/unsubscribe_user.ex b/lib/mix/tasks/unsubscribe_user.ex new file mode 100644 index 000000000..62ea61a5c --- /dev/null +++ b/lib/mix/tasks/unsubscribe_user.ex @@ -0,0 +1,38 @@ +defmodule Mix.Tasks.UnsubscribeUser do + use Mix.Task + alias Pleroma.{User, Repo} + require Logger + + @moduledoc """ + Deactivate and Unsubscribe local users from a user + + Usage: ``mix unsubscribe_user `` + + Example: ``mix unsubscribe_user lain`` + """ + def run([nickname]) do + Mix.Task.run("app.start") + + with %User{} = user <- User.get_by_nickname(nickname) do + Logger.info("Deactivating #{user.nickname}") + User.deactivate(user) + + {:ok, friends} = User.get_friends(user) + + Enum.each(friends, fn friend -> + user = Repo.get(User, user.id) + + Logger.info("Unsubscribing #{friend.nickname} from #{user.nickname}") + User.unfollow(user, friend) + end) + + :timer.sleep(500) + + user = Repo.get(User, user.id) + + if length(user.following) == 0 do + Logger.info("Successfully unsubscribed all followers from #{user.nickname}") + end + end + end +end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index a89728471..eedad7675 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -12,18 +12,35 @@ def start(_type, _args) do [ # Start the Ecto repository supervisor(Pleroma.Repo, []), + worker(Pleroma.Emoji, []), # Start the endpoint when the application starts supervisor(Pleroma.Web.Endpoint, []), # Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3) # worker(Pleroma.Worker, [arg1, arg2, arg3]), - worker(Cachex, [ - :user_cache, + worker( + Cachex, [ - default_ttl: 25000, - ttl_interval: 1000, - limit: 2500 - ] - ]), + :user_cache, + [ + default_ttl: 25000, + ttl_interval: 1000, + limit: 2500 + ] + ], + id: :cachex_user + ), + worker( + Cachex, + [ + :object_cache, + [ + default_ttl: 25000, + ttl_interval: 1000, + limit: 2500 + ] + ], + id: :cachex_object + ), worker( Cachex, [ @@ -40,8 +57,8 @@ def start(_type, _args) do id: :cachex_idem ), worker(Pleroma.Web.Federator, []), - worker(Pleroma.Gopher.Server, []), - worker(Pleroma.Stats, []) + worker(Pleroma.Stats, []), + worker(Pleroma.Gopher.Server, []) ] ++ if Mix.env() == :test, do: [], diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex new file mode 100644 index 000000000..15f771b6e --- /dev/null +++ b/lib/pleroma/config.ex @@ -0,0 +1,42 @@ +defmodule Pleroma.Config do + defmodule Error do + defexception [:message] + end + + def get(key), do: get(key, nil) + + def get([key], default), do: get(key, default) + + def get([parent_key | keys], default) do + Application.get_env(:pleroma, parent_key) + |> get_in(keys) || default + end + + def get(key, default) do + Application.get_env(:pleroma, key, default) + end + + def get!(key) do + value = get(key, nil) + + if value == nil do + raise(Error, message: "Missing configuration value: #{inspect(key)}") + else + value + end + end + + def put([key], value), do: put(key, value) + + def put([parent_key | keys], value) do + parent = + Application.get_env(:pleroma, parent_key) + |> put_in(keys, value) + + Application.put_env(:pleroma, parent_key, parent) + end + + def put(key, value) do + Application.put_env(:pleroma, key, value) + end +end diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex new file mode 100644 index 000000000..0a5e1d5ce --- /dev/null +++ b/lib/pleroma/emoji.ex @@ -0,0 +1,194 @@ +defmodule Pleroma.Emoji do + @moduledoc """ + The emojis are loaded from: + + * the built-in Finmojis (if enabled in configuration), + * the files: `config/emoji.txt` and `config/custom_emoji.txt` + * glob paths + + This GenServer stores in an ETS table the list of the loaded emojis, and also allows to reload the list at runtime. + """ + use GenServer + @ets __MODULE__.Ets + @ets_options [:set, :protected, :named_table, {:read_concurrency, true}] + + @doc false + def start_link() do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + @doc "Reloads the emojis from disk." + @spec reload() :: :ok + def reload() do + GenServer.call(__MODULE__, :reload) + end + + @doc "Returns the path of the emoji `name`." + @spec get(String.t()) :: String.t() | nil + def get(name) do + case :ets.lookup(@ets, name) do + [{_, path}] -> path + _ -> nil + end + end + + @doc "Returns all the emojos!!" + @spec get_all() :: [{String.t(), String.t()}, ...] + def get_all() do + :ets.tab2list(@ets) + end + + @doc false + def init(_) do + @ets = :ets.new(@ets, @ets_options) + GenServer.cast(self(), :reload) + {:ok, nil} + end + + @doc false + def handle_cast(:reload, state) do + load() + {:noreply, state} + end + + @doc false + def handle_call(:reload, _from, state) do + load() + {:reply, :ok, state} + end + + @doc false + def terminate(_, _) do + :ok + end + + @doc false + def code_change(_old_vsn, state, _extra) do + load() + {:ok, state} + end + + defp load() do + emojis = + (load_finmoji(Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled)) ++ + load_from_file("config/emoji.txt") ++ + load_from_file("config/custom_emoji.txt") ++ + load_from_globs( + Keyword.get(Application.get_env(:pleroma, :emoji, []), :shortcode_globs, []) + )) + |> Enum.reject(fn value -> value == nil end) + + true = :ets.insert(@ets, emojis) + :ok + end + + @finmoji [ + "a_trusted_friend", + "alandislands", + "association", + "auroraborealis", + "baby_in_a_box", + "bear", + "black_gold", + "christmasparty", + "crosscountryskiing", + "cupofcoffee", + "education", + "fashionista_finns", + "finnishlove", + "flag", + "forest", + "four_seasons_of_bbq", + "girlpower", + "handshake", + "happiness", + "headbanger", + "icebreaker", + "iceman", + "joulutorttu", + "kaamos", + "kalsarikannit_f", + "kalsarikannit_m", + "karjalanpiirakka", + "kicksled", + "kokko", + "lavatanssit", + "losthopes_f", + "losthopes_m", + "mattinykanen", + "meanwhileinfinland", + "moominmamma", + "nordicfamily", + "out_of_office", + "peacemaker", + "perkele", + "pesapallo", + "polarbear", + "pusa_hispida_saimensis", + "reindeer", + "sami", + "sauna_f", + "sauna_m", + "sauna_whisk", + "sisu", + "stuck", + "suomimainittu", + "superfood", + "swan", + "the_cap", + "the_conductor", + "the_king", + "the_voice", + "theoriginalsanta", + "tomoffinland", + "torillatavataan", + "unbreakable", + "waiting", + "white_nights", + "woollysocks" + ] + defp load_finmoji(true) do + Enum.map(@finmoji, fn finmoji -> + {finmoji, "/finmoji/128px/#{finmoji}-128.png"} + end) + end + + defp load_finmoji(_), do: [] + + defp load_from_file(file) do + if File.exists?(file) do + load_from_file_stream(File.stream!(file)) + else + [] + end + end + + defp load_from_file_stream(stream) do + stream + |> Stream.map(&String.strip/1) + |> Stream.map(fn line -> + case String.split(line, ~r/,\s*/) do + [name, file] -> {name, file} + _ -> nil + end + end) + |> Enum.to_list() + end + + defp load_from_globs(globs) do + static_path = Path.join(:code.priv_dir(:pleroma), "static") + + paths = + Enum.map(globs, fn glob -> + Path.join(static_path, glob) + |> Path.wildcard() + end) + |> Enum.concat() + + Enum.map(paths, fn path -> + shortcode = Path.basename(path, Path.extname(path)) + external_path = Path.join("/", Path.relative_to(path, static_path)) + {shortcode, external_path} + end) + end +end diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index fe904df3a..25ed38f34 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -36,6 +36,34 @@ def get_filters(%Pleroma.User{id: user_id} = user) do Repo.all(query) end + def create(%Pleroma.Filter{user_id: user_id, filter_id: nil} = filter) do + # If filter_id wasn't given, use the max filter_id for this user plus 1. + # XXX This could result in a race condition if a user tries to add two + # different filters for their account from two different clients at the + # same time, but that should be unlikely. + + max_id_query = + from( + f in Pleroma.Filter, + where: f.user_id == ^user_id, + select: max(f.filter_id) + ) + + filter_id = + case Repo.one(max_id_query) do + # Start allocating from 1 + nil -> + 1 + + max_id -> + max_id + 1 + end + + filter + |> Map.put(:filter_id, filter_id) + |> Repo.insert() + end + def create(%Pleroma.Filter{} = filter) do Repo.insert(filter) end diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index ecc102b62..26bb17377 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -2,6 +2,7 @@ defmodule Pleroma.Formatter do alias Pleroma.User alias Pleroma.Web.MediaProxy alias Pleroma.HTML + alias Pleroma.Emoji @tag_regex ~r/\#\w+/u def parse_tags(text, data \\ %{}) do @@ -28,125 +29,10 @@ def parse_mentions(text) do |> Enum.filter(fn {_match, user} -> user end) end - @finmoji [ - "a_trusted_friend", - "alandislands", - "association", - "auroraborealis", - "baby_in_a_box", - "bear", - "black_gold", - "christmasparty", - "crosscountryskiing", - "cupofcoffee", - "education", - "fashionista_finns", - "finnishlove", - "flag", - "forest", - "four_seasons_of_bbq", - "girlpower", - "handshake", - "happiness", - "headbanger", - "icebreaker", - "iceman", - "joulutorttu", - "kaamos", - "kalsarikannit_f", - "kalsarikannit_m", - "karjalanpiirakka", - "kicksled", - "kokko", - "lavatanssit", - "losthopes_f", - "losthopes_m", - "mattinykanen", - "meanwhileinfinland", - "moominmamma", - "nordicfamily", - "out_of_office", - "peacemaker", - "perkele", - "pesapallo", - "polarbear", - "pusa_hispida_saimensis", - "reindeer", - "sami", - "sauna_f", - "sauna_m", - "sauna_whisk", - "sisu", - "stuck", - "suomimainittu", - "superfood", - "swan", - "the_cap", - "the_conductor", - "the_king", - "the_voice", - "theoriginalsanta", - "tomoffinland", - "torillatavataan", - "unbreakable", - "waiting", - "white_nights", - "woollysocks" - ] + def emojify(text) do + emojify(text, Emoji.get_all()) + end - @instance Application.get_env(:pleroma, :instance) - - @finmoji_with_filenames (if Keyword.get(@instance, :finmoji_enabled) do - Enum.map(@finmoji, fn finmoji -> - {finmoji, "/finmoji/128px/#{finmoji}-128.png"} - end) - else - [] - end) - - @emoji_from_file (with {:ok, default} <- File.read("config/emoji.txt") do - custom = - with {:ok, custom} <- File.read("config/custom_emoji.txt") do - custom - else - _e -> "" - end - - (default <> "\n" <> custom) - |> String.trim() - |> String.split(~r/\n+/) - |> Enum.map(fn line -> - [name, file] = String.split(line, ~r/,\s*/) - {name, file} - end) - else - _ -> [] - end) - - @emoji_from_globs ( - static_path = Path.join(:code.priv_dir(:pleroma), "static") - - globs = - Application.get_env(:pleroma, :emoji, []) - |> Keyword.get(:shortcode_globs, []) - - paths = - Enum.map(globs, fn glob -> - Path.join(static_path, glob) - |> Path.wildcard() - end) - |> Enum.concat() - - Enum.map(paths, fn path -> - shortcode = Path.basename(path, Path.extname(path)) - external_path = Path.join("/", Path.relative_to(path, static_path)) - {shortcode, external_path} - end) - ) - - @emoji @finmoji_with_filenames ++ @emoji_from_globs ++ @emoji_from_file - - def emojify(text, emoji \\ @emoji) def emojify(text, nil), do: text def emojify(text, emoji) do @@ -166,15 +52,11 @@ def emojify(text, emoji) do end def get_emoji(text) when is_binary(text) do - Enum.filter(@emoji, fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end) + Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end) end def get_emoji(_), do: [] - def get_custom_emoji() do - @emoji - end - @link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui @uri_schemes Application.get_env(:pleroma, :uri_schemes, []) diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index d34037f4f..e6361a82c 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -1,16 +1,16 @@ defmodule Pleroma.Gopher.Server do use GenServer require Logger - @gopher Application.get_env(:pleroma, :gopher) def start_link() do - ip = Keyword.get(@gopher, :ip, {0, 0, 0, 0}) - port = Keyword.get(@gopher, :port, 1234) + config = Pleroma.Config.get(:gopher, []) + ip = Keyword.get(config, :ip, {0, 0, 0, 0}) + port = Keyword.get(config, :port, 1234) GenServer.start_link(__MODULE__, [ip, port], []) end def init([ip, port]) do - if Keyword.get(@gopher, :enabled, false) do + if Pleroma.Config.get([:gopher, :enabled], false) do Logger.info("Starting gopher server on #{port}") :ranch.start_listener( @@ -37,9 +37,6 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do alias Pleroma.Repo alias Pleroma.HTML - @instance Application.get_env(:pleroma, :instance) - @gopher Application.get_env(:pleroma, :gopher) - def start_link(ref, socket, transport, opts) do pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts]) {:ok, pid} @@ -62,7 +59,7 @@ def info(text) do def link(name, selector, type \\ 1) do address = Pleroma.Web.Endpoint.host() - port = Keyword.get(@gopher, :port, 1234) + port = Pleroma.Config.get([:gopher, :port], 1234) "#{type}#{name}\t#{selector}\t#{address}\t#{port}\r\n" end @@ -85,7 +82,7 @@ def render_activities(activities) do end def response("") do - info("Welcome to #{Keyword.get(@instance, :name, "Pleroma")}!") <> + info("Welcome to #{Pleroma.Config.get([:instance, :name], "Pleroma")}!") <> link("Public Timeline", "/main/public") <> link("Federated Timeline", "/main/all") <> ".\r\n" end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index a7338eac3..1b920d7fd 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -1,14 +1,12 @@ defmodule Pleroma.HTML do alias HtmlSanitizeEx.Scrubber - @markup Application.get_env(:pleroma, :markup) - defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber] defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default] def get_scrubbers() do - Keyword.get(@markup, :scrub_policy) + Pleroma.Config.get([:markup, :scrub_policy]) |> get_scrubbers end @@ -91,6 +89,8 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes) Meta.allow_tag_with_these_attributes("a", ["name", "title"]) + Meta.allow_tag_with_these_attributes("abbr", ["title"]) + Meta.allow_tag_with_these_attributes("b", []) Meta.allow_tag_with_these_attributes("blockquote", []) Meta.allow_tag_with_these_attributes("br", []) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index e0dcd9823..a3aeb1221 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -1,6 +1,6 @@ defmodule Pleroma.Notification do use Ecto.Schema - alias Pleroma.{User, Activity, Notification, Repo} + alias Pleroma.{User, Activity, Notification, Repo, Object} import Ecto.Query schema "notifications" do @@ -42,6 +42,20 @@ def for_user(user, opts \\ %{}) do Repo.all(query) end + def set_read_up_to(%{id: user_id} = _user, id) do + query = + from( + n in Notification, + where: n.user_id == ^user_id, + where: n.id <= ^id, + update: [ + set: [seen: true] + ] + ) + + Repo.update_all(query, []) + end + def get(%{id: user_id} = _user, id) do query = from( @@ -81,7 +95,7 @@ def dismiss(%{id: user_id} = _user, id) do def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity) when type in ["Create", "Like", "Announce", "Follow"] do - users = User.get_notified_from_activity(activity) + users = get_notified_from_activity(activity) notifications = Enum.map(users, fn user -> create_notification(activity, user) end) {:ok, notifications} @@ -99,4 +113,64 @@ def create_notification(%Activity{} = activity, %User{} = user) do notification end end + + def get_notified_from_activity(activity, local_only \\ true) + + def get_notified_from_activity( + %Activity{data: %{"to" => _, "type" => type} = data} = activity, + local_only + ) + when type in ["Create", "Like", "Announce", "Follow"] do + recipients = + [] + |> maybe_notify_to_recipients(activity) + |> maybe_notify_mentioned_recipients(activity) + |> Enum.uniq() + + User.get_users_from_set(recipients, local_only) + end + + def get_notified_from_activity(_, local_only), do: [] + + defp maybe_notify_to_recipients( + recipients, + %Activity{data: %{"to" => to, "type" => type}} = activity + ) do + recipients ++ to + end + + defp maybe_notify_mentioned_recipients( + recipients, + %Activity{data: %{"to" => to, "type" => type} = data} = activity + ) + when type == "Create" do + object = Object.normalize(data["object"]) + + object_data = + cond do + !is_nil(object) -> + object.data + + is_map(data["object"]) -> + data["object"] + + true -> + %{} + end + + tagged_mentions = maybe_extract_mentions(object_data) + + recipients ++ tagged_mentions + end + + defp maybe_notify_mentioned_recipients(recipients, _), do: recipients + + defp maybe_extract_mentions(%{"tag" => tag}) do + tag + |> Enum.filter(fn x -> is_map(x) end) + |> Enum.filter(fn x -> x["type"] == "Mention" end) + |> Enum.map(fn x -> x["href"] end) + end + + defp maybe_extract_mentions(_), do: [] end diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 1bcff5a7b..067ecfaf4 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -1,6 +1,6 @@ defmodule Pleroma.Object do use Ecto.Schema - alias Pleroma.{Repo, Object} + alias Pleroma.{Repo, Object, Activity} import Ecto.{Query, Changeset} schema "objects" do @@ -37,7 +37,7 @@ def get_cached_by_ap_id(ap_id) do else key = "object:#{ap_id}" - Cachex.fetch!(:user_cache, key, fn _ -> + Cachex.fetch!(:object_cache, key, fn _ -> object = get_by_ap_id(ap_id) if object do @@ -52,4 +52,12 @@ def get_cached_by_ap_id(ap_id) do def context_mapping(context) do Object.change(%Object{}, %{data: %{"id" => context}}) end + + def delete(%Object{data: %{"id" => id}} = object) do + with Repo.delete(object), + Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)), + {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do + {:ok, object} + end + end end diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex new file mode 100644 index 000000000..4108d90af --- /dev/null +++ b/lib/pleroma/plugs/federating_plug.ex @@ -0,0 +1,18 @@ +defmodule Pleroma.Web.FederatingPlug do + import Plug.Conn + + def init(options) do + options + end + + def call(conn, opts) do + if Keyword.get(Application.get_env(:pleroma, :instance), :federating) do + conn + else + conn + |> put_status(404) + |> Phoenix.Controller.render(Pleroma.Web.ErrorView, "404.json") + |> halt() + end + end +end diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex new file mode 100644 index 000000000..960c7f6bf --- /dev/null +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -0,0 +1,59 @@ +defmodule Pleroma.Plugs.HTTPSecurityPlug do + alias Pleroma.Config + import Plug.Conn + + def init(opts), do: opts + + def call(conn, options) do + if Config.get([:http_security, :enabled]) do + conn = + merge_resp_headers(conn, headers()) + |> maybe_send_sts_header(Config.get([:http_security, :sts])) + else + conn + end + end + + defp headers do + referrer_policy = Config.get([:http_security, :referrer_policy]) + + [ + {"x-xss-protection", "1; mode=block"}, + {"x-permitted-cross-domain-policies", "none"}, + {"x-frame-options", "DENY"}, + {"x-content-type-options", "nosniff"}, + {"referrer-policy", referrer_policy}, + {"x-download-options", "noopen"}, + {"content-security-policy", csp_string() <> ";"} + ] + end + + defp csp_string do + [ + "default-src 'none'", + "base-uri 'self'", + "form-action *", + "frame-ancestors 'none'", + "img-src 'self' data: https:", + "media-src 'self' https:", + "style-src 'self' 'unsafe-inline'", + "font-src 'self'", + "script-src 'self'", + "connect-src 'self' " <> String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"), + "upgrade-insecure-requests" + ] + |> Enum.join("; ") + end + + defp maybe_send_sts_header(conn, true) do + max_age_sts = Config.get([:http_security, :sts_max_age]) + max_age_ct = Config.get([:http_security, :ct_max_age]) + + merge_resp_headers(conn, [ + {"strict-transport-security", "max-age=#{max_age_sts}; includeSubDomains"}, + {"expect-ct", "enforce, max-age=#{max_age_ct}"} + ]) + end + + defp maybe_send_sts_header(conn, _), do: conn +end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index f188a5f32..89aa779f9 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -1,64 +1,74 @@ defmodule Pleroma.Upload do alias Ecto.UUID - @storage_backend Application.get_env(:pleroma, Pleroma.Upload) - |> Keyword.fetch!(:uploader) + def check_file_size(path, nil), do: true - def store(%Plug.Upload{} = file, should_dedupe) do - content_type = get_content_type(file.path) - - uuid = get_uuid(file, should_dedupe) - name = get_name(file, uuid, content_type, should_dedupe) - - strip_exif_data(content_type, file.path) - - {:ok, url_path} = - @storage_backend.put_file(name, uuid, file.path, content_type, should_dedupe) - - %{ - "type" => "Document", - "url" => [ - %{ - "type" => "Link", - "mediaType" => content_type, - "href" => url_path - } - ], - "name" => name - } + def check_file_size(path, size_limit) do + {:ok, %{size: size}} = File.stat(path) + size <= size_limit end - def store(%{"img" => "data:image/" <> image_data}, should_dedupe) do + def store(file, should_dedupe, size_limit \\ nil) + + def store(%Plug.Upload{} = file, should_dedupe, size_limit) do + content_type = get_content_type(file.path) + + with uuid <- get_uuid(file, should_dedupe), + name <- get_name(file, uuid, content_type, should_dedupe), + true <- check_file_size(file.path, size_limit) do + strip_exif_data(content_type, file.path) + + {:ok, url_path} = uploader().put_file(name, uuid, file.path, content_type, should_dedupe) + + %{ + "type" => "Document", + "url" => [ + %{ + "type" => "Link", + "mediaType" => content_type, + "href" => url_path + } + ], + "name" => name + } + else + _e -> nil + end + end + + def store(%{"img" => "data:image/" <> image_data}, should_dedupe, size_limit) do parsed = Regex.named_captures(~r/(?jpeg|png|gif);base64,(?.*)/, image_data) data = Base.decode64!(parsed["data"], ignore: :whitespace) - tmp_path = tempfile_for_image(data) + with tmp_path <- tempfile_for_image(data), + uuid <- UUID.generate(), + true <- check_file_size(tmp_path, size_limit) do + content_type = get_content_type(tmp_path) + strip_exif_data(content_type, tmp_path) - uuid = UUID.generate() + name = + create_name( + String.downcase(Base.encode16(:crypto.hash(:sha256, data))), + parsed["filetype"], + content_type + ) - content_type = get_content_type(tmp_path) - strip_exif_data(content_type, tmp_path) + {:ok, url_path} = uploader().put_file(name, uuid, tmp_path, content_type, should_dedupe) - name = - create_name( - String.downcase(Base.encode16(:crypto.hash(:sha256, data))), - parsed["filetype"], - content_type - ) - - {:ok, url_path} = @storage_backend.put_file(name, uuid, tmp_path, content_type, should_dedupe) - - %{ - "type" => "Image", - "url" => [ - %{ - "type" => "Link", - "mediaType" => content_type, - "href" => url_path - } - ], - "name" => name - } + %{ + "type" => "Image", + "url" => [ + %{ + "type" => "Link", + "mediaType" => content_type, + "href" => url_path + } + ], + "name" => name + } + else + _e -> nil + end end @doc """ @@ -167,4 +177,8 @@ def get_content_type(file) do _e -> "application/octet-stream" end end + + defp uploader() do + Pleroma.Config.get!([Pleroma.Upload, :uploader]) + end end diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex index 87322753d..40a836460 100644 --- a/lib/pleroma/uploaders/s3.ex +++ b/lib/pleroma/uploaders/s3.ex @@ -1,10 +1,13 @@ defmodule Pleroma.Uploaders.S3 do + alias Pleroma.Web.MediaProxy + @behaviour Pleroma.Uploaders.Uploader def put_file(name, uuid, path, content_type, _should_dedupe) do settings = Application.get_env(:pleroma, Pleroma.Uploaders.S3) bucket = Keyword.fetch!(settings, :bucket) public_endpoint = Keyword.fetch!(settings, :public_endpoint) + force_media_proxy = Keyword.fetch!(settings, :force_media_proxy) {:ok, file_data} = File.read(path) @@ -19,7 +22,16 @@ def put_file(name, uuid, path, content_type, _should_dedupe) do ]) |> ExAws.request() - {:ok, "#{public_endpoint}/#{bucket}/#{s3_name}"} + url_base = "#{public_endpoint}/#{bucket}/#{s3_name}" + + public_url = + if force_media_proxy do + MediaProxy.url(url_base) + else + url_base + end + + {:ok, public_url} end defp encode(name) do diff --git a/lib/pleroma/uploaders/swift/keystone.ex b/lib/pleroma/uploaders/swift/keystone.ex index a79214319..e578b3c61 100644 --- a/lib/pleroma/uploaders/swift/keystone.ex +++ b/lib/pleroma/uploaders/swift/keystone.ex @@ -1,11 +1,9 @@ defmodule Pleroma.Uploaders.Swift.Keystone do use HTTPoison.Base - @settings Application.get_env(:pleroma, Pleroma.Uploaders.Swift) - def process_url(url) do Enum.join( - [Keyword.fetch!(@settings, :auth_url), url], + [Pleroma.Config.get!([Pleroma.Uploaders.Swift, :auth_url]), url], "/" ) end @@ -16,9 +14,10 @@ def process_response_body(body) do end def get_token() do - username = Keyword.fetch!(@settings, :username) - password = Keyword.fetch!(@settings, :password) - tenant_id = Keyword.fetch!(@settings, :tenant_id) + settings = Pleroma.Config.get(Pleroma.Uploaders.Swift) + username = Keyword.fetch!(settings, :username) + password = Keyword.fetch!(settings, :password) + tenant_id = Keyword.fetch!(settings, :tenant_id) case post( "/tokens", diff --git a/lib/pleroma/uploaders/swift/swift.ex b/lib/pleroma/uploaders/swift/swift.ex index 819dfebda..fa08ca966 100644 --- a/lib/pleroma/uploaders/swift/swift.ex +++ b/lib/pleroma/uploaders/swift/swift.ex @@ -1,17 +1,15 @@ defmodule Pleroma.Uploaders.Swift.Client do use HTTPoison.Base - @settings Application.get_env(:pleroma, Pleroma.Uploaders.Swift) - def process_url(url) do Enum.join( - [Keyword.fetch!(@settings, :storage_url), url], + [Pleroma.Config.get!([Pleroma.Uploaders.Swift, :storage_url]), url], "/" ) end def upload_file(filename, body, content_type) do - object_url = Keyword.fetch!(@settings, :object_url) + object_url = Pleroma.Config.get!([Pleroma.Uploaders.Swift, :object_url]) token = Pleroma.Uploaders.Swift.Keystone.get_token() case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 0c9fa559a..be634a8e1 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -295,6 +295,7 @@ def update_and_set_cache(changeset) do def invalidate_cache(user) do Cachex.del(:user_cache, "ap_id:#{user.ap_id}") Cachex.del(:user_cache, "nickname:#{user.nickname}") + Cachex.del(:user_cache, "user_info:#{user.id}") end def get_cached_by_ap_id(ap_id) do @@ -463,36 +464,25 @@ def update_follower_count(%User{} = user) do update_and_set_cache(cs) end - def get_notified_from_activity_query(to) do + def get_users_from_set_query(ap_ids, false) do from( u in User, - where: u.ap_id in ^to, + where: u.ap_id in ^ap_ids + ) + end + + def get_users_from_set_query(ap_ids, true) do + query = get_users_from_set_query(ap_ids, false) + + from( + u in query, where: u.local == true ) end - def get_notified_from_activity(%Activity{recipients: to, data: %{"type" => "Announce"} = data}) do - object = Object.normalize(data["object"]) - actor = User.get_cached_by_ap_id(data["actor"]) - - # ensure that the actor who published the announced object appears only once - to = - if actor.nickname != nil do - to ++ [object.data["actor"]] - else - to - end - |> Enum.uniq() - - query = get_notified_from_activity_query(to) - - Repo.all(query) - end - - def get_notified_from_activity(%Activity{recipients: to}) do - query = get_notified_from_activity_query(to) - - Repo.all(query) + def get_users_from_set(ap_ids, local_only \\ true) do + get_users_from_set_query(ap_ids, local_only) + |> Repo.all() end def get_recipients_from_activity(%Activity{recipients: to}) do @@ -622,8 +612,8 @@ def moderator_user_query() do ) end - def deactivate(%User{} = user) do - new_info = Map.put(user.info, "deactivated", true) + def deactivate(%User{} = user, status \\ true) do + new_info = Map.put(user.info, "deactivated", status) cs = User.info_changeset(user, %{info: new_info}) update_and_set_cache(cs) end @@ -656,7 +646,7 @@ def delete(%User{} = user) do end end) - :ok + {:ok, user} end def html_filter_policy(%User{info: %{"no_rich_text" => true}}) do diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 173ca688d..c6733e487 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -10,8 +10,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do @httpoison Application.get_env(:pleroma, :httpoison) - @instance Application.get_env(:pleroma, :instance) - # For Announce activities, we filter the recipients based on following status for any actors # that match actual users. See issue #164 for more information about why this is necessary. defp get_recipients(%{"type" => "Announce"} = data) do @@ -44,7 +42,7 @@ defp get_recipients(data) do defp check_actor_is_active(actor) do if not is_nil(actor) do with user <- User.get_cached_by_ap_id(actor), - nil <- user.info["deactivated"] do + false <- !!user.info["deactivated"] do :ok else _e -> :reject @@ -273,8 +271,7 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru "to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"] } - with Repo.delete(object), - Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)), + with {:ok, _} <- Object.delete(object), {:ok, activity} <- insert(data, local), :ok <- maybe_federate(activity), {:ok, _actor} <- User.decrease_note_count(user) do @@ -575,9 +572,12 @@ def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do |> Enum.reverse() end - def upload(file) do - data = Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media]) - Repo.insert(%Object{data: data}) + def upload(file, size_limit \\ nil) do + with data <- + Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media], size_limit), + false <- is_nil(data) do + Repo.insert(%Object{data: data}) + end end def user_data_from_user_object(data) do @@ -657,14 +657,12 @@ def make_user_from_nickname(nickname) do end end - @quarantined_instances Keyword.get(@instance, :quarantined_instances, []) - def should_federate?(inbox, public) do if public do true else inbox_info = URI.parse(inbox) - inbox_info.host not in @quarantined_instances + !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host) end end @@ -793,9 +791,10 @@ def entire_thread_visible_for_user?(nil, user), do: false # child def entire_thread_visible_for_user?( - %Activity{data: %{"object" => %{"inReplyTo" => _parent_id}}} = tail, + %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail, user - ) do + ) + when is_binary(parent_id) do parent = Activity.get_in_reply_to_activity(tail) visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user) end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index a7b1c0079..3570a75cb 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -11,6 +11,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do action_fallback(:errors) + plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay]) + plug(:relay_active? when action in [:relay]) + + def relay_active?(conn, _) do + if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do + conn + else + conn + |> put_status(404) + |> json(%{error: "not found"}) + |> halt + end + end + def user(conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_cached_by_nickname(nickname), {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex index b4f91f3cc..c53cb1ad2 100644 --- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex +++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex @@ -3,10 +3,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do @behaviour Pleroma.Web.ActivityPub.MRF - @mrf_normalize_markup Application.get_env(:pleroma, :mrf_normalize_markup) - def filter(%{"type" => activity_type} = object) when activity_type == "Create" do - scrub_policy = Keyword.get(@mrf_normalize_markup, :scrub_policy) + scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy]) child = object["object"] diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index 129d04617..627284083 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -2,10 +2,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do alias Pleroma.User @behaviour Pleroma.Web.ActivityPub.MRF - @mrf_rejectnonpublic Application.get_env(:pleroma, :mrf_rejectnonpublic) - @allow_followersonly Keyword.get(@mrf_rejectnonpublic, :allow_followersonly) - @allow_direct Keyword.get(@mrf_rejectnonpublic, :allow_direct) - @impl true def filter(%{"type" => "Create"} = object) do user = User.get_cached_by_ap_id(object["actor"]) @@ -20,6 +16,8 @@ def filter(%{"type" => "Create"} = object) do true -> "direct" end + policy = Pleroma.Config.get(:mrf_rejectnonpublic) + case visibility do "public" -> {:ok, object} @@ -28,14 +26,14 @@ def filter(%{"type" => "Create"} = object) do {:ok, object} "followers" -> - with true <- @allow_followersonly do + with true <- Keyword.get(policy, :allow_followersonly) do {:ok, object} else _e -> {:reject, nil} end "direct" -> - with true <- @allow_direct do + with true <- Keyword.get(policy, :allow_direct) do {:ok, object} else _e -> {:reject, nil} diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 319721d48..86dcf5080 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -2,60 +2,76 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do alias Pleroma.User @behaviour Pleroma.Web.ActivityPub.MRF - @mrf_policy Application.get_env(:pleroma, :mrf_simple) + defp check_accept(%{host: actor_host} = _actor_info, object) do + accepts = Pleroma.Config.get([:mrf_simple, :accept]) - @accept Keyword.get(@mrf_policy, :accept) - defp check_accept(%{host: actor_host} = actor_info, object) - when length(@accept) > 0 and not (actor_host in @accept) do - {:reject, nil} + cond do + accepts == [] -> {:ok, object} + actor_host == Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object} + Enum.member?(accepts, actor_host) -> {:ok, object} + true -> {:reject, nil} + end end - defp check_accept(actor_info, object), do: {:ok, object} - - @reject Keyword.get(@mrf_policy, :reject) - defp check_reject(%{host: actor_host} = actor_info, object) when actor_host in @reject do - {:reject, nil} + defp check_reject(%{host: actor_host} = _actor_info, object) do + if Enum.member?(Pleroma.Config.get([:mrf_simple, :reject]), actor_host) do + {:reject, nil} + else + {:ok, object} + end end - defp check_reject(actor_info, object), do: {:ok, object} + defp check_media_removal( + %{host: actor_host} = _actor_info, + %{"type" => "Create", "object" => %{"attachement" => child_attachment}} = object + ) + when length(child_attachment) > 0 do + object = + if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_removal]), actor_host) do + child_object = Map.delete(object["object"], "attachment") + Map.put(object, "object", child_object) + else + object + end - @media_removal Keyword.get(@mrf_policy, :media_removal) - defp check_media_removal(%{host: actor_host} = actor_info, %{"type" => "Create"} = object) - when actor_host in @media_removal do - child_object = Map.delete(object["object"], "attachment") - object = Map.put(object, "object", child_object) {:ok, object} end - defp check_media_removal(actor_info, object), do: {:ok, object} + defp check_media_removal(_actor_info, object), do: {:ok, object} - @media_nsfw Keyword.get(@mrf_policy, :media_nsfw) defp check_media_nsfw( - %{host: actor_host} = actor_info, + %{host: actor_host} = _actor_info, %{ "type" => "Create", "object" => %{"attachment" => child_attachment} = child_object } = object ) - when actor_host in @media_nsfw and length(child_attachment) > 0 do - tags = (child_object["tag"] || []) ++ ["nsfw"] - child_object = Map.put(child_object, "tags", tags) - child_object = Map.put(child_object, "sensitive", true) - object = Map.put(object, "object", child_object) + when length(child_attachment) > 0 do + object = + if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_nsfw]), actor_host) do + tags = (child_object["tag"] || []) ++ ["nsfw"] + child_object = Map.put(child_object, "tags", tags) + child_object = Map.put(child_object, "sensitive", true) + Map.put(object, "object", child_object) + else + object + end + {:ok, object} end - defp check_media_nsfw(actor_info, object), do: {:ok, object} + defp check_media_nsfw(_actor_info, object), do: {:ok, object} - @ftl_removal Keyword.get(@mrf_policy, :federated_timeline_removal) - defp check_ftl_removal(%{host: actor_host} = actor_info, object) - when actor_host in @ftl_removal do - user = User.get_by_ap_id(object["actor"]) - - # flip to/cc relationship to make the post unlisted + defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do object = - if "https://www.w3.org/ns/activitystreams#Public" in object["to"] and - user.follower_address in object["cc"] do + with true <- + Enum.member?( + Pleroma.Config.get([:mrf_simple, :federated_timeline_removal]), + 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 to = List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address] @@ -68,14 +84,12 @@ defp check_ftl_removal(%{host: actor_host} = actor_info, object) |> Map.put("to", to) |> Map.put("cc", cc) else - object + _ -> object end {:ok, object} end - defp check_ftl_removal(actor_info, object), do: {:ok, object} - @impl true def filter(object) do actor_info = URI.parse(object["actor"]) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index cbc800ad6..d51d8626b 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -57,6 +57,7 @@ def fix_object(object) do object |> fix_actor |> fix_attachments + |> fix_url |> fix_context |> fix_in_reply_to |> fix_emoji @@ -171,6 +172,27 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachme def fix_attachments(object), do: object + def fix_url(%{"url" => url} = object) when is_map(url) do + object + |> Map.put("url", url["href"]) + end + + def fix_url(%{"url" => url} = object) when is_list(url) do + first_element = Enum.at(url, 0) + + url_string = + cond do + is_bitstring(first_element) -> first_element + is_map(first_element) -> first_element["href"] || "" + true -> "" + end + + object + |> Map.put("url", url_string) + end + + def fix_url(object), do: object + def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end) @@ -241,7 +263,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"] do + when objtype in ["Article", "Note", "Video", "Page"] do actor = get_actor(data) data = @@ -484,9 +506,6 @@ def handle_incoming( end end - @ap_config Application.get_env(:pleroma, :activitypub) - @accept_blocks Keyword.get(@ap_config, :accept_blocks) - def handle_incoming( %{ "type" => "Undo", @@ -495,7 +514,7 @@ def handle_incoming( "id" => id } = _data ) do - with true <- @accept_blocks, + with true <- Pleroma.Config.get([:activitypub, :accept_blocks]), %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked), %User{} = blocker <- User.get_or_fetch_by_ap_id(blocker), {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do @@ -509,7 +528,7 @@ def handle_incoming( def handle_incoming( %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data ) do - with true <- @accept_blocks, + with true <- Pleroma.Config.get([:activitypub, :accept_blocks]), %User{local: true} = blocked = User.get_cached_by_ap_id(blocked), %User{} = blocker = User.get_or_fetch_by_ap_id(blocker), {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do @@ -570,6 +589,8 @@ def prepare_object(object) do |> prepare_attachments |> set_conversation |> set_reply_to_uri + |> strip_internal_fields + |> strip_internal_tags end # @doc @@ -585,7 +606,7 @@ def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = obj data = data |> Map.put("object", object) - |> Map.put("@context", "https://www.w3.org/ns/activitystreams") + |> Map.merge(Utils.make_json_ld_header()) {:ok, data} end @@ -604,7 +625,7 @@ def prepare_outgoing(%{"type" => "Accept"} = data) do data = data |> Map.put("object", object) - |> Map.put("@context", "https://www.w3.org/ns/activitystreams") + |> Map.merge(Utils.make_json_ld_header()) {:ok, data} end @@ -622,7 +643,7 @@ def prepare_outgoing(%{"type" => "Reject"} = data) do data = data |> Map.put("object", object) - |> Map.put("@context", "https://www.w3.org/ns/activitystreams") + |> Map.merge(Utils.make_json_ld_header()) {:ok, data} end @@ -632,7 +653,7 @@ def prepare_outgoing(%{"type" => _type} = data) do data = data |> maybe_fix_object_url - |> Map.put("@context", "https://www.w3.org/ns/activitystreams") + |> Map.merge(Utils.make_json_ld_header()) {:ok, data} end @@ -674,12 +695,9 @@ def add_hashtags(object) do end def add_mention_tags(object) do - recipients = object["to"] ++ (object["cc"] || []) - mentions = - recipients - |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end) - |> Enum.filter(& &1) + object + |> Utils.get_notified_from_object() |> Enum.map(fn user -> %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"} end) @@ -739,6 +757,29 @@ def prepare_attachments(object) do |> Map.put("attachment", attachments) end + defp strip_internal_fields(object) do + object + |> Map.drop([ + "likes", + "like_count", + "announcements", + "announcement_count", + "emoji", + "context_id" + ]) + end + + defp strip_internal_tags(%{"tag" => tags} = object) do + tags = + tags + |> Enum.filter(fn x -> is_map(x) end) + + object + |> Map.put("tag", tags) + end + + defp strip_internal_tags(object), do: object + defp user_upgrade_task(user) do old_follower_address = User.ap_followers(user) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index d6ac2dd8c..549148989 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -1,11 +1,13 @@ defmodule Pleroma.Web.ActivityPub.Utils do - alias Pleroma.{Repo, Web, Object, Activity, User} + alias Pleroma.{Repo, Web, Object, Activity, User, Notification} alias Pleroma.Web.Router.Helpers alias Pleroma.Web.Endpoint alias Ecto.{Changeset, UUID} import Ecto.Query require Logger + @supported_object_types ["Article", "Note", "Video", "Page"] + # Some implementations send the actor URI as the actor field, others send the entire actor object, # so figure out what the actor's URI is based on what we have. def get_ap_id(object) do @@ -70,18 +72,7 @@ def make_json_ld_header do %{ "@context" => [ "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - %{ - "manuallyApprovesFollowers" => "as:manuallyApprovesFollowers", - "sensitive" => "as:sensitive", - "Hashtag" => "as:Hashtag", - "ostatus" => "http://ostatus.org#", - "atomUri" => "ostatus:atomUri", - "inReplyToAtomUri" => "ostatus:inReplyToAtomUri", - "conversation" => "ostatus:conversation", - "toot" => "http://joinmastodon.org/ns#", - "Emoji" => "toot:Emoji" - } + "#{Web.base_url()}/schemas/litepub-0.1.jsonld" ] } end @@ -106,6 +97,21 @@ def generate_id(type) do "#{Web.base_url()}/#{type}/#{UUID.generate()}" end + def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do + fake_create_activity = %{ + "to" => object["to"], + "cc" => object["cc"], + "type" => "Create", + "object" => object + } + + Notification.get_notified_from_activity(%Activity{data: fake_create_activity}, false) + end + + def get_notified_from_object(object) do + Notification.get_notified_from_activity(%Activity{data: object}, false) + end + def create_context(context) do context = context || generate_id("contexts") changeset = Object.context_mapping(context) @@ -175,7 +181,7 @@ def lazy_put_object_defaults(map, activity \\ %{}) do Inserts a full object if it is contained in an activity. """ def insert_full_object(%{"object" => %{"type" => type} = object_data}) - when is_map(object_data) and type in ["Article", "Note", "Video"] do + when is_map(object_data) and type in @supported_object_types do with {:ok, _} <- Object.create(object_data) do :ok end diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex index cc0b0556b..1911ddfb7 100644 --- a/lib/pleroma/web/activity_pub/views/object_view.ex +++ b/lib/pleroma/web/activity_pub/views/object_view.ex @@ -1,27 +1,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do use Pleroma.Web, :view + alias Pleroma.{Object, Activity} alias Pleroma.Web.ActivityPub.Transmogrifier - def render("object.json", %{object: object}) do - base = %{ - "@context" => [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - %{ - "manuallyApprovesFollowers" => "as:manuallyApprovesFollowers", - "sensitive" => "as:sensitive", - "Hashtag" => "as:Hashtag", - "ostatus" => "http://ostatus.org#", - "atomUri" => "ostatus:atomUri", - "inReplyToAtomUri" => "ostatus:inReplyToAtomUri", - "conversation" => "ostatus:conversation", - "toot" => "http://joinmastodon.org/ns#", - "Emoji" => "toot:Emoji" - } - ] - } + def render("object.json", %{object: %Object{} = object}) do + base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header() additional = Transmogrifier.prepare_object(object.data) Map.merge(base, additional) end + + def render("object.json", %{object: %Activity{} = activity}) do + base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header() + object = Object.normalize(activity.data["object"]) + + additional = + Transmogrifier.prepare_object(activity.data) + |> Map.put("object", Transmogrifier.prepare_object(object.data)) + + Map.merge(base, additional) + end end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 16419e1b7..eb335813d 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -17,7 +17,6 @@ def render("user.json", %{user: %{nickname: nil} = user}) do public_key = :public_key.pem_encode([public_key]) %{ - "@context" => "https://www.w3.org/ns/activitystreams", "id" => user.ap_id, "type" => "Application", "following" => "#{user.ap_id}/following", @@ -36,6 +35,7 @@ def render("user.json", %{user: %{nickname: nil} = user}) do "sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox" } } + |> Map.merge(Utils.make_json_ld_header()) end def render("user.json", %{user: user}) do diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index f8fef219f..77e4dbbd7 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -36,7 +36,6 @@ def unrepeat(id_or_ap_id, user) do def favorite(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), - false <- activity.data["actor"] == user.ap_id, object <- Object.normalize(activity.data["object"]["id"]) do ActivityPub.like(user, object) else @@ -47,7 +46,6 @@ def favorite(id_or_ap_id, user) do def unfavorite(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), - false <- activity.data["actor"] == user.ap_id, object <- Object.normalize(activity.data["object"]["id"]) do ActivityPub.unlike(user, object) else @@ -72,15 +70,17 @@ def get_visibility(%{"in_reply_to_status_id" => status_id}) when not is_nil(stat def get_visibility(_), do: "public" - @instance Application.get_env(:pleroma, :instance) - @allowed_post_formats Keyword.get(@instance, :allowed_post_formats) + defp get_content_type(content_type) do + if Enum.member?(Pleroma.Config.get([:instance, :allowed_post_formats]), content_type) do + content_type + else + "text/plain" + end + end - defp get_content_type(content_type) when content_type in @allowed_post_formats, do: content_type - defp get_content_type(_), do: "text/plain" - - @limit Keyword.get(@instance, :limit) def post(user, %{"status" => status} = data) do visibility = get_visibility(data) + limit = Pleroma.Config.get([:instance, :limit]) with status <- String.trim(status), attachments <- attachments_from_ids(data["media_ids"]), @@ -100,7 +100,7 @@ def post(user, %{"status" => status} = data) do context <- make_context(inReplyTo), cw <- data["spoiler_text"], full_payload <- String.trim(status <> (data["spoiler_text"] || "")), - length when length in 1..@limit <- String.length(full_payload), + length when length in 1..limit <- String.length(full_payload), object <- make_note_data( user.ap_id, diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 4cbbd0c7d..728f24c7e 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -2,6 +2,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do alias Pleroma.{Repo, Object, Formatter, Activity} alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.Endpoint + alias Pleroma.Web.MediaProxy alias Pleroma.User alias Calendar.Strftime alias Comeonin.Pbkdf2 @@ -18,6 +19,8 @@ def get_by_id_or_ap_id(id) do end end + def get_replied_to_activity(""), do: nil + def get_replied_to_activity(id) when not is_nil(id) do Repo.get(Activity, id) end @@ -31,21 +34,29 @@ def attachments_from_ids(ids) do end def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do - to = ["https://www.w3.org/ns/activitystreams#Public"] - mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end) - cc = [user.follower_address | mentioned_users] + + to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users] + cc = [user.follower_address] if inReplyTo do - {to, Enum.uniq([inReplyTo.data["actor"] | cc])} + {Enum.uniq([inReplyTo.data["actor"] | to]), cc} else {to, cc} end end def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do - {to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "public") - {cc, to} + mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end) + + to = [user.follower_address | mentioned_users] + cc = ["https://www.w3.org/ns/activitystreams#Public"] + + if inReplyTo do + {Enum.uniq([inReplyTo.data["actor"] | to]), cc} + else + {to, cc} + end end def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do @@ -88,8 +99,9 @@ def maybe_add_attachments(text, attachments, _no_links) do def add_attachments(text, attachments) do attachment_text = Enum.map(attachments, fn - %{"url" => [%{"href" => href} | _]} -> - name = URI.decode(Path.basename(href)) + %{"url" => [%{"href" => href} | _]} = attachment -> + name = attachment["name"] || URI.decode(Path.basename(href)) + href = MediaProxy.url(href) "#{shortname(name)}" _ -> diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 955bd61f3..85bb4ff5f 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -11,13 +11,17 @@ defmodule Pleroma.Web.Endpoint do # # You should set gzip to true if you are running phoenix.digest # when deploying your static files in production. + plug(CORSPlug) + plug(Pleroma.Plugs.HTTPSecurityPlug) + plug(Plug.Static, at: "/media", from: Pleroma.Uploaders.Local.upload_path(), gzip: false) plug( Plug.Static, at: "/", from: :pleroma, - only: ~w(index.html static finmoji emoji packs sounds images instance sw.js favicon.png) + only: + ~w(index.html static finmoji emoji packs sounds images instance sw.js favicon.png schemas) ) # Code reloading can be explicitly enabled under the @@ -42,13 +46,18 @@ defmodule Pleroma.Web.Endpoint do plug(Plug.MethodOverride) plug(Plug.Head) + cookie_name = + if Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag), + do: "__Host-pleroma_key", + else: "pleroma_key" + # The session will be stored in the cookie and signed, # this means its contents can be read but not tampered with. # Set :encryption_salt if you would also like to encrypt it. plug( Plug.Session, store: :cookie, - key: "_pleroma_key", + key: cookie_name, signing_salt: "CqaoopA2", http_only: true, secure: diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index 078f3ec11..962cacfa3 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -7,13 +7,12 @@ defmodule Pleroma.Web.Federator do alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.OStatus require Logger @websub Application.get_env(:pleroma, :websub) @ostatus Application.get_env(:pleroma, :ostatus) @httpoison Application.get_env(:pleroma, :httpoison) - @instance Application.get_env(:pleroma, :instance) - @federating Keyword.get(@instance, :federating) @max_jobs 20 def init(args) do @@ -65,15 +64,17 @@ def handle(:publish, activity) do {:ok, actor} = WebFinger.ensure_keys_present(actor) if ActivityPub.is_public?(activity) do - Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end) - Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) + if OStatus.is_representable?(activity) do + Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end) + Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) - Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end) - Pleroma.Web.Salmon.publish(actor, activity) + Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end) + Pleroma.Web.Salmon.publish(actor, activity) + end - if Mix.env() != :test do + if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) - Pleroma.Web.ActivityPub.Relay.publish(activity) + Relay.publish(activity) end end @@ -147,7 +148,7 @@ def handle(type, _) do end def enqueue(type, payload, priority \\ 1) do - if @federating do + if Pleroma.Config.get([:instance, :federating]) do if Mix.env() == :test do handle(type, payload) else diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index bc7558cb8..a0b74311b 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -35,6 +35,14 @@ def create_app(conn, params) do def update_credentials(%{assigns: %{user: user}} = conn, params) do original_user = user + avatar_upload_limit = + Application.get_env(:pleroma, :instance) + |> Keyword.fetch(:avatar_upload_limit) + + banner_upload_limit = + Application.get_env(:pleroma, :instance) + |> Keyword.fetch(:banner_upload_limit) + params = if bio = params["note"] do Map.put(params, "bio", bio) @@ -52,7 +60,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do user = if avatar = params["avatar"] do with %Plug.Upload{} <- avatar, - {:ok, object} <- ActivityPub.upload(avatar), + {:ok, object} <- ActivityPub.upload(avatar, avatar_upload_limit), change = Ecto.Changeset.change(user, %{avatar: object.data}), {:ok, user} = User.update_and_set_cache(change) do user @@ -66,7 +74,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do user = if banner = params["header"] do with %Plug.Upload{} <- banner, - {:ok, object} <- ActivityPub.upload(banner), + {:ok, object} <- ActivityPub.upload(banner, banner_upload_limit), new_info <- Map.put(user.info, "banner", object.data), change <- User.info_changeset(user, %{info: new_info}), {:ok, user} <- User.update_and_set_cache(change) do @@ -124,22 +132,23 @@ def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do end end - @instance Application.get_env(:pleroma, :instance) @mastodon_api_level "2.5.0" def masto_instance(conn, _params) do + instance = Pleroma.Config.get(:instance) + response = %{ uri: Web.base_url(), - title: Keyword.get(@instance, :name), - description: Keyword.get(@instance, :description), - version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})", - email: Keyword.get(@instance, :email), + title: Keyword.get(instance, :name), + description: Keyword.get(instance, :description), + version: "#{@mastodon_api_level} (compatible; #{Keyword.get(instance, :version)})", + email: Keyword.get(instance, :email), urls: %{ streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws") }, stats: Stats.get_stats(), thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg", - max_toot_chars: Keyword.get(@instance, :limit) + max_toot_chars: Keyword.get(instance, :limit) } json(conn, response) @@ -150,7 +159,7 @@ def peers(conn, _params) do end defp mastodonized_emoji do - Pleroma.Formatter.get_custom_emoji() + Pleroma.Emoji.get_all() |> Enum.map(fn {shortcode, relative_url} -> url = to_string(URI.merge(Web.base_url(), relative_url)) @@ -269,9 +278,12 @@ def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do end end - def dm_timeline(%{assigns: %{user: user}} = conn, _params) do + def dm_timeline(%{assigns: %{user: user}} = conn, params) do query = - ActivityPub.fetch_activities_query([user.ap_id], %{"type" => "Create", visibility: "direct"}) + ActivityPub.fetch_activities_query( + [user.ap_id], + Map.merge(params, %{"type" => "Create", visibility: "direct"}) + ) activities = Repo.all(query) @@ -435,6 +447,12 @@ def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do render(conn, AccountView, "relationships.json", %{user: user, targets: targets}) end + # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array. + def relationships(%{assigns: %{user: user}} = conn, _) do + conn + |> json([]) + end + def update_media(%{assigns: %{user: _}} = conn, data) do with %Object{} = object <- Repo.get(Object, data["id"]), true <- is_binary(data["description"]), @@ -500,6 +518,7 @@ def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do |> Map.put("type", "Create") |> Map.put("local_only", local_only) |> Map.put("blocking_user", user) + |> Map.put("tag", String.downcase(params["tag"])) activities = ActivityPub.fetch_public_activities(params) @@ -572,15 +591,16 @@ def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) d end end - @activitypub Application.get_env(:pleroma, :activitypub) - @follow_handshake_timeout Keyword.get(@activitypub, :follow_handshake_timeout) - def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do with %User{} = followed <- Repo.get(User, id), {:ok, follower} <- User.maybe_direct_follow(follower, followed), {:ok, _activity} <- ActivityPub.follow(follower, followed), {:ok, follower, followed} <- - User.wait_and_refresh(@follow_handshake_timeout, follower, followed) do + User.wait_and_refresh( + Pleroma.Config.get([:activitypub, :follow_handshake_timeout]), + follower, + followed + ) do render(conn, AccountView, "relationship.json", %{user: follower, target: followed}) else {:error, message} -> @@ -871,6 +891,8 @@ def index(%{assigns: %{user: user}} = conn, _params) do if user && token do mastodon_emoji = mastodonized_emoji() + limit = Pleroma.Config.get([:instance, :limit]) + accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) @@ -890,7 +912,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do auto_play_gif: false, display_sensitive_media: false, reduce_motion: false, - max_toot_chars: Keyword.get(@instance, :limit) + max_toot_chars: limit }, rights: %{ delete_others_notice: !!user.info["is_moderator"] @@ -950,7 +972,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do push_subscription: nil, accounts: accounts, custom_emojis: mastodon_emoji, - char_limit: Keyword.get(@instance, :limit) + char_limit: limit } |> Jason.encode!() @@ -976,9 +998,29 @@ def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _para end end + def login(conn, %{"code" => code}) do + with {:ok, app} <- get_or_make_app(), + %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id), + {:ok, token} <- Token.exchange_token(app, auth) do + conn + |> put_session(:oauth_token, token.token) + |> redirect(to: "/web/getting-started") + end + end + def login(conn, _) do - conn - |> render(MastodonView, "login.html", %{error: false}) + with {:ok, app} <- get_or_make_app() do + path = + o_auth_path(conn, :authorize, + response_type: "code", + client_id: app.client_id, + redirect_uri: ".", + scope: app.scopes + ) + + conn + |> redirect(to: path) + end end defp get_or_make_app() do @@ -997,22 +1039,6 @@ defp get_or_make_app() do end end - def login_post(conn, %{"authorization" => %{"name" => name, "password" => password}}) do - with %User{} = user <- User.get_by_nickname_or_email(name), - true <- Pbkdf2.checkpw(password, user.password_hash), - {:ok, app} <- get_or_make_app(), - {:ok, auth} <- Authorization.create_authorization(app, user), - {:ok, token} <- Token.exchange_token(app, auth) do - conn - |> put_session(:oauth_token, token.token) - |> redirect(to: "/web/getting-started") - else - _e -> - conn - |> render(MastodonView, "login.html", %{error: "Wrong username or password"}) - end - end - def logout(conn, _) do conn |> clear_session @@ -1156,18 +1182,15 @@ def errors(conn, _) do |> json("Something went wrong") end - @suggestions Application.get_env(:pleroma, :suggestions) - def suggestions(%{assigns: %{user: user}} = conn, _) do - if Keyword.get(@suggestions, :enabled, false) do - api = Keyword.get(@suggestions, :third_party_engine, "") - timeout = Keyword.get(@suggestions, :timeout, 5000) - limit = Keyword.get(@suggestions, :limit, 23) + suggestions = Pleroma.Config.get(:suggestions) - host = - Application.get_env(:pleroma, Pleroma.Web.Endpoint) - |> Keyword.get(:url) - |> Keyword.get(:host) + if Keyword.get(suggestions, :enabled, false) do + api = Keyword.get(suggestions, :third_party_engine, "") + timeout = Keyword.get(suggestions, :timeout, 5000) + limit = Keyword.get(suggestions, :limit, 23) + + host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) user = user.nickname url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user) diff --git a/lib/pleroma/web/mastodon_api/mastodon_socket.ex b/lib/pleroma/web/mastodon_api/mastodon_socket.ex index bc628ba56..0f3d5ff7c 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_socket.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_socket.ex @@ -26,15 +26,19 @@ def connect(params, socket) do "list", "hashtag" ] <- params["stream"] do - topic = if stream == "list", do: "list:#{params["list"]}", else: stream - socket_stream = if stream == "hashtag", do: "hashtag:#{params["tag"]}", else: stream + topic = + case stream do + "hashtag" -> "hashtag:#{params["tag"]}" + "list" -> "list:#{params["list"]}" + _ -> stream + end socket = socket |> assign(:topic, topic) |> assign(:user, user) - Pleroma.Web.Streamer.add_socket(socket_stream, socket) + Pleroma.Web.Streamer.add_socket(topic, socket) {:ok, socket} else _e -> :error diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 8ffaf8466..2d9a915f0 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -61,7 +61,7 @@ def render( in_reply_to_id: nil, in_reply_to_account_id: nil, reblog: reblogged, - content: reblogged[:content], + content: reblogged[:content] || "", created_at: created_at, reblogs_count: 0, replies_count: 0, @@ -166,7 +166,7 @@ def render("status.json", _) do def render("attachment.json", %{attachment: attachment}) do [attachment_url | _] = attachment["url"] media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image" - href = attachment_url["href"] + href = attachment_url["href"] |> MediaProxy.url() type = cond do @@ -180,9 +180,9 @@ def render("attachment.json", %{attachment: attachment}) do %{ id: to_string(attachment["id"] || hash_id), - url: MediaProxy.url(href), + url: href, remote_url: href, - preview_url: MediaProxy.url(href), + preview_url: href, text_url: href, type: type, description: attachment["name"] @@ -230,24 +230,24 @@ def render_content(%{"type" => "Video"} = object) do if !!name and name != "" do "

#{name}

#{object["content"]}" else - object["content"] + object["content"] || "" end content end - def render_content(%{"type" => "Article"} = object) do + def render_content(%{"type" => object_type} = object) when object_type in ["Article", "Page"] do summary = object["name"] content = - if !!summary and summary != "" do + if !!summary and summary != "" and is_bitstring(object["url"]) do "

#{summary}

#{object["content"]}" else - object["content"] + object["content"] || "" end content end - def render_content(object), do: object["content"] + def render_content(object), do: object["content"] || "" end diff --git a/lib/pleroma/web/media_proxy/controller.ex b/lib/pleroma/web/media_proxy/controller.ex index 8195a665e..bb257c262 100644 --- a/lib/pleroma/web/media_proxy/controller.ex +++ b/lib/pleroma/web/media_proxy/controller.ex @@ -11,15 +11,47 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do error: "public, must-revalidate, max-age=160" } - def remote(conn, %{"sig" => sig, "url" => url}) do + # Content-types that will not be returned as content-disposition attachments + # Override with :media_proxy, :safe_content_types in the configuration + @safe_content_types [ + "image/gif", + "image/jpeg", + "image/jpg", + "image/png", + "image/svg+xml", + "audio/mpeg", + "audio/mp3", + "video/webm", + "video/mp4" + ] + + def remote(conn, params = %{"sig" => sig, "url" => url}) do config = Application.get_env(:pleroma, :media_proxy, []) with true <- Keyword.get(config, :enabled, false), {:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url), - {:ok, content_type, body} <- proxy_request(url) do + filename <- Path.basename(URI.parse(url).path), + true <- + if(Map.get(params, "filename"), + do: filename == Path.basename(conn.request_path), + else: true + ), + {:ok, content_type, body} <- proxy_request(url), + safe_content_type <- + Enum.member?( + Keyword.get(config, :safe_content_types, @safe_content_types), + content_type + ) do conn |> put_resp_content_type(content_type) |> set_cache_header(:default) + |> put_resp_header( + "content-security-policy", + "default-src 'none'; style-src 'unsafe-inline'; media-src data:; img-src 'self' data:" + ) + |> put_resp_header("x-xss-protection", "1; mode=block") + |> put_resp_header("x-content-type-options", "nosniff") + |> put_attachement_header(safe_content_type, filename) |> send_resp(200, body) else false -> @@ -92,6 +124,12 @@ defp proxy_request_body(client, _) do # TODO: the body is passed here as well because some hosts do not provide a content-type. # At some point we may want to use magic numbers to discover the content-type and reply a proper one. defp proxy_request_content_type(headers, _body) do - headers["Content-Type"] || headers["content-type"] || "image/jpeg" + headers["Content-Type"] || headers["content-type"] || "application/octet-stream" + end + + defp put_attachement_header(conn, true, _), do: conn + + defp put_attachement_header(conn, false, filename) do + put_resp_header(conn, "content-disposition", "attachment; filename='#{filename}'") end end diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 37718f48b..93c36b4ed 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -15,7 +15,10 @@ def url(url) do base64 = Base.url_encode64(url, @base64_opts) sig = :crypto.hmac(:sha, secret, base64) sig64 = sig |> Base.url_encode64(@base64_opts) - Keyword.get(config, :base_url, Pleroma.Web.base_url()) <> "/proxy/#{sig64}/#{base64}" + filename = Path.basename(URI.parse(url).path) + + Keyword.get(config, :base_url, Pleroma.Web.base_url()) <> + "/proxy/#{sig64}/#{base64}/#{filename}" end end diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 59b0ce3e1..d58f08881 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do alias Pleroma.{User, Repo} alias Pleroma.Web.ActivityPub.MRF + plug(Pleroma.Web.FederatingPlug) + def schemas(conn, _params) do response = %{ links: [ @@ -113,6 +115,12 @@ def nodeinfo(conn, %{"version" => "2.0"}) do staffAccounts: staff_accounts, federation: federation_response, postFormats: Keyword.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) + }, features: features } } diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 5441ee0a8..d03c8b05a 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -33,25 +33,35 @@ def create_authorization(conn, %{ true <- Pbkdf2.checkpw(password, user.password_hash), %App{} = app <- Repo.get_by(App, client_id: client_id), {:ok, auth} <- Authorization.create_authorization(app, user) do - if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do - render(conn, "results.html", %{ - auth: auth - }) - else - connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" - url = "#{redirect_uri}#{connector}" - url_params = %{:code => auth.token} + # Special case: Local MastodonFE. + redirect_uri = + if redirect_uri == "." do + mastodon_api_url(conn, :login) + else + redirect_uri + end - url_params = - if params["state"] do - Map.put(url_params, :state, params["state"]) - else - url_params - end + cond do + redirect_uri == "urn:ietf:wg:oauth:2.0:oob" -> + render(conn, "results.html", %{ + auth: auth + }) - url = "#{url}#{Plug.Conn.Query.encode(url_params)}" + true -> + connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" + url = "#{redirect_uri}#{connector}" + url_params = %{:code => auth.token} - redirect(conn, external: url) + url_params = + if params["state"] do + Map.put(url_params, :state, params["state"]) + else + url_params + end + + url = "#{url}#{Plug.Conn.Query.encode(url_params)}" + + redirect(conn, external: url) end end end @@ -133,8 +143,11 @@ def token_revoke(conn, %{"token" => token} = params) do end end + # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be + # decoding it. Investigate sometime. defp fix_padding(token) do token + |> URI.decode() |> Base.url_decode64!(padding: false) |> Base.url_encode64() end diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 916c894eb..1d0019d3b 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -11,6 +11,21 @@ defmodule Pleroma.Web.OStatus do alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler} alias Pleroma.Web.ActivityPub.Transmogrifier + def is_representable?(%Activity{data: data}) do + object = Object.normalize(data["object"]) + + cond do + is_nil(object) -> + false + + object.data["type"] == "Note" -> + true + + true -> + false + end + end + def feed_path(user) do "#{user.ap_id}/feed.atom" end diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 09d1b1110..2f92935e7 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ActivityPub + plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming]) action_fallback(:errors) def feed_redirect(conn, %{"nickname" => nickname}) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index b531b6188..5e81db00b 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -3,12 +3,6 @@ defmodule Pleroma.Web.Router do alias Pleroma.{Repo, User, Web.Router} - @instance Application.get_env(:pleroma, :instance) - @federating Keyword.get(@instance, :federating) - @allow_relay Keyword.get(@instance, :allow_relay) - @public Keyword.get(@instance, :public) - @registrations_open Keyword.get(@instance, :registrations_open) - pipeline :api do plug(:accepts, ["json"]) plug(:fetch_session) @@ -243,11 +237,7 @@ defmodule Pleroma.Web.Router do end scope "/api", Pleroma.Web do - if @public do - pipe_through(:api) - else - pipe_through(:authenticated_api) - end + pipe_through(:api) get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline) @@ -280,8 +270,13 @@ defmodule Pleroma.Web.Router do get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline) get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline) get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline) + get("/statuses/dm_timeline", TwitterAPI.Controller, :dm_timeline) get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications) + # XXX: this is really a pleroma API, but we want to keep the pleroma namespace clean + # for now. + post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read) + post("/statuses/update", TwitterAPI.Controller, :status_update) post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet) post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet) @@ -331,12 +326,10 @@ defmodule Pleroma.Web.Router do get("/users/:nickname/feed", OStatus.OStatusController, :feed) get("/users/:nickname", OStatus.OStatusController, :feed_redirect) - if @federating do - post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming) - post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request) - get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation) - post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming) - end + post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming) + post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request) + get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation) + post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming) end pipeline :activitypub do @@ -353,31 +346,27 @@ defmodule Pleroma.Web.Router do get("/users/:nickname/outbox", ActivityPubController, :outbox) end - if @federating do - if @allow_relay do - scope "/relay", Pleroma.Web.ActivityPub do - pipe_through(:ap_relay) - get("/", ActivityPubController, :relay) - end - end + scope "/relay", Pleroma.Web.ActivityPub do + pipe_through(:ap_relay) + get("/", ActivityPubController, :relay) + end - scope "/", Pleroma.Web.ActivityPub do - pipe_through(:activitypub) - post("/users/:nickname/inbox", ActivityPubController, :inbox) - post("/inbox", ActivityPubController, :inbox) - end + scope "/", Pleroma.Web.ActivityPub do + pipe_through(:activitypub) + post("/users/:nickname/inbox", ActivityPubController, :inbox) + post("/inbox", ActivityPubController, :inbox) + end - scope "/.well-known", Pleroma.Web do - pipe_through(:well_known) + scope "/.well-known", Pleroma.Web do + pipe_through(:well_known) - get("/host-meta", WebFinger.WebFingerController, :host_meta) - get("/webfinger", WebFinger.WebFingerController, :webfinger) - get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas) - end + get("/host-meta", WebFinger.WebFingerController, :host_meta) + get("/webfinger", WebFinger.WebFingerController, :webfinger) + get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas) + end - scope "/nodeinfo", Pleroma.Web do - get("/:version", Nodeinfo.NodeinfoController, :nodeinfo) - end + scope "/nodeinfo", Pleroma.Web do + get("/:version", Nodeinfo.NodeinfoController, :nodeinfo) end scope "/", Pleroma.Web.MastodonAPI do @@ -390,12 +379,12 @@ defmodule Pleroma.Web.Router do end pipeline :remote_media do - plug(:accepts, ["html"]) end scope "/proxy/", Pleroma.Web.MediaProxy do pipe_through(:remote_media) get("/:sig/:url", MediaProxyController, :remote) + get("/:sig/:url/:filename", MediaProxyController, :remote) end scope "/", Fallback do diff --git a/lib/pleroma/web/templates/mastodon_api/mastodon/login.html.eex b/lib/pleroma/web/templates/mastodon_api/mastodon/login.html.eex deleted file mode 100644 index 34cd7ed89..000000000 --- a/lib/pleroma/web/templates/mastodon_api/mastodon/login.html.eex +++ /dev/null @@ -1,11 +0,0 @@ -

Login to Mastodon Frontend

-<%= if @error do %> -

<%= @error %>

-<% end %> -<%= form_for @conn, mastodon_api_path(@conn, :login), [as: "authorization"], fn f -> %> -<%= text_input f, :name, placeholder: "Username or email" %> -
-<%= password_input f, :password, placeholder: "Password" %> -
-<%= submit "Log in" %> -<% end %> diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 01cd17121..dc4a864d6 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Web.WebFinger alias Pleroma.Web.CommonAPI alias Comeonin.Pbkdf2 - alias Pleroma.Formatter + alias Pleroma.{Formatter, Emoji} alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.{Repo, PasswordResetToken, User} @@ -134,19 +134,20 @@ def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id} end end - @instance Application.get_env(:pleroma, :instance) - @instance_fe Application.get_env(:pleroma, :fe) - @instance_chat Application.get_env(:pleroma, :chat) def config(conn, _params) do + instance = Pleroma.Config.get(:instance) + instance_fe = Pleroma.Config.get(:fe) + instance_chat = Pleroma.Config.get(:chat) + case get_format(conn) do "xml" -> response = """ - #{Keyword.get(@instance, :name)} + #{Keyword.get(instance, :name)} #{Web.base_url()} - #{Keyword.get(@instance, :limit)} - #{!Keyword.get(@instance, :registrations_open)} + #{Keyword.get(instance, :limit)} + #{!Keyword.get(instance, :registrations_open)} """ @@ -157,32 +158,32 @@ def config(conn, _params) do _ -> data = %{ - name: Keyword.get(@instance, :name), - description: Keyword.get(@instance, :description), + name: Keyword.get(instance, :name), + description: Keyword.get(instance, :description), server: Web.base_url(), - textlimit: to_string(Keyword.get(@instance, :limit)), - closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1"), - private: if(Keyword.get(@instance, :public, true), do: "0", else: "1") + textlimit: to_string(Keyword.get(instance, :limit)), + closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"), + private: if(Keyword.get(instance, :public, true), do: "0", else: "1") } pleroma_fe = %{ - theme: Keyword.get(@instance_fe, :theme), - background: Keyword.get(@instance_fe, :background), - logo: Keyword.get(@instance_fe, :logo), - logoMask: Keyword.get(@instance_fe, :logo_mask), - logoMargin: Keyword.get(@instance_fe, :logo_margin), - redirectRootNoLogin: Keyword.get(@instance_fe, :redirect_root_no_login), - redirectRootLogin: Keyword.get(@instance_fe, :redirect_root_login), - chatDisabled: !Keyword.get(@instance_chat, :enabled), - showInstanceSpecificPanel: Keyword.get(@instance_fe, :show_instance_panel), - scopeOptionsEnabled: Keyword.get(@instance_fe, :scope_options_enabled), - formattingOptionsEnabled: Keyword.get(@instance_fe, :formatting_options_enabled), - collapseMessageWithSubject: Keyword.get(@instance_fe, :collapse_message_with_subject), - hidePostStats: Keyword.get(@instance_fe, :hide_post_stats), - hideUserStats: Keyword.get(@instance_fe, :hide_user_stats) + theme: Keyword.get(instance_fe, :theme), + background: Keyword.get(instance_fe, :background), + logo: Keyword.get(instance_fe, :logo), + logoMask: Keyword.get(instance_fe, :logo_mask), + logoMargin: Keyword.get(instance_fe, :logo_margin), + redirectRootNoLogin: Keyword.get(instance_fe, :redirect_root_no_login), + redirectRootLogin: Keyword.get(instance_fe, :redirect_root_login), + chatDisabled: !Keyword.get(instance_chat, :enabled), + showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel), + scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled), + formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled), + collapseMessageWithSubject: Keyword.get(instance_fe, :collapse_message_with_subject), + hidePostStats: Keyword.get(instance_fe, :hide_post_stats), + hideUserStats: Keyword.get(instance_fe, :hide_user_stats) } - managed_config = Keyword.get(@instance, :managed_config) + managed_config = Keyword.get(instance, :managed_config) data = if managed_config do @@ -196,7 +197,7 @@ def config(conn, _params) do end def version(conn, _params) do - version = Keyword.get(@instance, :version) + version = Pleroma.Config.get([:instance, :version]) case get_format(conn) do "xml" -> @@ -212,7 +213,7 @@ def version(conn, _params) do end def emoji(conn, _params) do - json(conn, Enum.into(Formatter.get_custom_emoji(), %{})) + json(conn, Enum.into(Emoji.get_all(), %{})) end def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 3747285da..5bfb83b1e 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -3,11 +3,10 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.TwitterAPI.UserView alias Pleroma.Web.{OStatus, CommonAPI} + alias Pleroma.Web.MediaProxy import Ecto.Query - @instance Application.get_env(:pleroma, :instance) @httpoison Application.get_env(:pleroma, :httpoison) - @registrations_open Keyword.get(@instance, :registrations_open) def create_status(%User{} = user, %{"status" => _} = data) do CommonAPI.post(user, data) @@ -20,15 +19,16 @@ def delete(%User{} = user, id) do end end - @activitypub Application.get_env(:pleroma, :activitypub) - @follow_handshake_timeout Keyword.get(@activitypub, :follow_handshake_timeout) - def follow(%User{} = follower, params) do with {:ok, %User{} = followed} <- get_user(params), {:ok, follower} <- User.maybe_direct_follow(follower, followed), {:ok, activity} <- ActivityPub.follow(follower, followed), {:ok, follower, followed} <- - User.wait_and_refresh(@follow_handshake_timeout, follower, followed) do + User.wait_and_refresh( + Pleroma.Config.get([:activitypub, :follow_handshake_timeout]), + follower, + followed + ) do {:ok, follower, followed, activity} else err -> err @@ -97,7 +97,7 @@ def upload(%Plug.Upload{} = file, format \\ "xml") do {:ok, object} = ActivityPub.upload(file) url = List.first(object.data["url"]) - href = url["href"] + href = url["href"] |> MediaProxy.url() type = url["mediaType"] case format do @@ -138,18 +138,20 @@ def register_user(params) do password_confirmation: params["confirm"] } + registrations_open = Pleroma.Config.get([:instance, :registrations_open]) + # no need to query DB if registration is open token = - unless @registrations_open || is_nil(tokenString) do + unless registrations_open || is_nil(tokenString) do Repo.get_by(UserInviteToken, %{token: tokenString}) end cond do - @registrations_open || (!is_nil(token) && !token.used) -> + registrations_open || (!is_nil(token) && !token.used) -> changeset = User.register_changeset(%User{}, params) with {:ok, user} <- Repo.insert(changeset) do - !@registrations_open && UserInviteToken.mark_as_used(token.token) + !registrations_open && UserInviteToken.mark_as_used(token.token) {:ok, user} else {:error, changeset} -> @@ -160,10 +162,10 @@ def register_user(params) do {:error, %{error: errors}} end - !@registrations_open && is_nil(token) -> + !registrations_open && is_nil(token) -> {:error, "Invalid token"} - !@registrations_open && token.used -> + !registrations_open && token.used -> {:error, "Expired token"} end end diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 4fc32b50c..dfcafdcc9 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do require Logger + plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline]) action_fallback(:errors) def verify_credentials(%{assigns: %{user: user}} = conn, _params) do @@ -125,6 +126,19 @@ def mentions_timeline(%{assigns: %{user: user}} = conn, params) do |> render(ActivityView, "index.json", %{activities: activities, for: user}) end + def dm_timeline(%{assigns: %{user: user}} = conn, params) do + query = + ActivityPub.fetch_activities_query( + [user.ap_id], + Map.merge(params, %{"type" => "Create", visibility: "direct"}) + ) + + activities = Repo.all(query) + + conn + |> render(ActivityView, "index.json", %{activities: activities, for: user}) + end + def notifications(%{assigns: %{user: user}} = conn, params) do notifications = Notification.for_user(user, params) @@ -132,6 +146,19 @@ def notifications(%{assigns: %{user: user}} = conn, params) do |> render(NotificationView, "notification.json", %{notifications: notifications, for: user}) end + def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do + Notification.set_read_up_to(user, latest_id) + + notifications = Notification.for_user(user, params) + + conn + |> render(NotificationView, "notification.json", %{notifications: notifications, for: user}) + end + + def notifications_read(%{assigns: %{user: user}} = conn, _) do + bad_request_reply(conn, "You need to specify latest_id") + end + def follow(%{assigns: %{user: user}} = conn, params) do case TwitterAPI.follow(user, params) do {:ok, user, followed, _activity} -> @@ -263,7 +290,11 @@ def register(conn, params) do end def update_avatar(%{assigns: %{user: user}} = conn, params) do - {:ok, object} = ActivityPub.upload(params) + upload_limit = + Application.get_env(:pleroma, :instance) + |> Keyword.fetch(:avatar_upload_limit) + + {:ok, object} = ActivityPub.upload(params, upload_limit) change = Changeset.change(user, %{avatar: object.data}) {:ok, user} = User.update_and_set_cache(change) CommonAPI.update(user) @@ -272,7 +303,11 @@ def update_avatar(%{assigns: %{user: user}} = conn, params) do end def update_banner(%{assigns: %{user: user}} = conn, params) do - with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}), + upload_limit = + Application.get_env(:pleroma, :instance) + |> Keyword.fetch(:banner_upload_limit) + + with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, upload_limit), new_info <- Map.put(user.info, "banner", object.data), change <- User.info_changeset(user, %{info: new_info}), {:ok, user} <- User.update_and_set_cache(change) do @@ -286,7 +321,11 @@ def update_banner(%{assigns: %{user: user}} = conn, params) do end def update_background(%{assigns: %{user: user}} = conn, params) do - with {:ok, object} <- ActivityPub.upload(params), + upload_limit = + Application.get_env(:pleroma, :instance) + |> Keyword.fetch(:background_upload_limit) + + with {:ok, object} <- ActivityPub.upload(params, upload_limit), new_info <- Map.put(user.info, "background", object.data), change <- User.info_changeset(user, %{info: new_info}), {:ok, _user} <- User.update_and_set_cache(change) do @@ -506,6 +545,18 @@ defp forbidden_json_reply(conn, error_message) do json_reply(conn, 403, json) end + def only_if_public_instance(conn = %{conn: %{assigns: %{user: _user}}}, _), do: conn + + def only_if_public_instance(conn, _) do + if Keyword.get(Application.get_env(:pleroma, :instance), :public) do + conn + else + conn + |> forbidden_json_reply("Invalid credentials.") + |> halt() + end + end + defp error_json(conn, error_message) do %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!() end diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index fb97f199b..83e8fb765 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -283,11 +283,11 @@ def render_content(%{"type" => "Note"} = object) do {summary, content} end - def render_content(%{"type" => "Article"} = object) do + def render_content(%{"type" => object_type} = object) when object_type in ["Article", "Page"] do summary = object["name"] || object["summary"] content = - if !!summary and summary != "" do + if !!summary and summary != "" and is_bitstring(object["url"]) do "

#{summary}

#{object["content"]}" else object["content"] diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index a662f83b6..a100a1127 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -55,8 +55,12 @@ def render("user.json", %{user: user = %User{}} = assigns) do "statusnet_blocking" => statusnet_blocking, "friends_count" => user_info[:following_count], "id" => user.id, - "name" => user.name, - "name_html" => HTML.strip_tags(user.name) |> Formatter.emojify(emoji), + "name" => user.name || user.nickname, + "name_html" => + if(user.name, + do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji), + else: user.nickname + ), "profile_image_url" => image, "profile_image_url_https" => image, "profile_image_url_profile_size" => image, diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex index 50d816256..002353166 100644 --- a/lib/pleroma/web/web_finger/web_finger_controller.ex +++ b/lib/pleroma/web/web_finger/web_finger_controller.ex @@ -3,6 +3,8 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do alias Pleroma.Web.WebFinger + plug(Pleroma.Web.FederatingPlug) + def host_meta(conn, _params) do xml = WebFinger.host_meta() diff --git a/lib/pleroma/web/websub/websub_controller.ex b/lib/pleroma/web/websub/websub_controller.ex index 590dd74a1..c1934ba92 100644 --- a/lib/pleroma/web/websub/websub_controller.ex +++ b/lib/pleroma/web/websub/websub_controller.ex @@ -5,6 +5,15 @@ defmodule Pleroma.Web.Websub.WebsubController do alias Pleroma.Web.Websub.WebsubClientSubscription require Logger + plug( + Pleroma.Web.FederatingPlug + when action in [ + :websub_subscription_request, + :websub_subscription_confirmation, + :websub_incoming + ] + ) + def websub_subscription_request(conn, %{"nickname" => nickname} = params) do user = User.get_cached_by_nickname(nickname) diff --git a/mix.exs b/mix.exs index 885df0094..ded414da9 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,19 @@ def project do compilers: [:phoenix, :gettext] ++ Mix.compilers(), start_permanent: Mix.env() == :prod, aliases: aliases(), - deps: deps() + deps: deps(), + + # Docs + name: "Pleroma", + source_url: "https://git.pleroma.social/pleroma/pleroma", + source_url_pattern: + "https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}", + homepage_url: "https://pleroma.social/", + docs: [ + logo: "priv/static/static/logo.png", + extras: ["README.md", "config/config.md"], + main: "readme" + ] ] end @@ -53,7 +65,9 @@ defp deps do {:credo, "~> 0.9.3", only: [:dev, :test]}, {:mock, "~> 0.3.1", only: :test}, {:crypt, - git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"} + git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"}, + {:cors_plug, "~> 1.5"}, + {:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false} ] end diff --git a/mix.lock b/mix.lock index 105eaf665..c0fa892a5 100644 --- a/mix.lock +++ b/mix.lock @@ -5,6 +5,7 @@ "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, + "cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, @@ -23,6 +24,7 @@ "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, @@ -30,6 +32,7 @@ "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/priv/static/index.html b/priv/static/index.html index 8ba786ec1..aa2ba6154 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -1 +1 @@ -Pleroma
\ No newline at end of file +Pleroma
\ No newline at end of file diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld new file mode 100644 index 000000000..819d25c38 --- /dev/null +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -0,0 +1,23 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Emoji": "toot:Emoji", + "Hashtag": "as:Hashtag", + "PropertyValue": "schema:PropertyValue", + "atomUri": "ostatus:atomUri", + "conversation": { + "@id": "ostatus:conversation", + "@type": "@id" + }, + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "ostatus": "http://ostatus.org#", + "schema": "http://schema.org", + "toot": "http://joinmastodon.org/ns#", + "totalItems": "as:totalItems", + "value": "schema:value", + "sensitive": "as:sensitive" + } + ] +} diff --git a/priv/static/static/config.json b/priv/static/static/config.json index a6eace0f8..69a707415 100644 --- a/priv/static/static/config.json +++ b/priv/static/static/config.json @@ -12,5 +12,6 @@ "formattingOptionsEnabled": false, "collapseMessageWithSubject": false, "hidePostStats": false, - "hideUserStats": false + "hideUserStats": false, + "loginMethod": "password" } diff --git a/priv/static/static/js/app.a65abd01bcd13a691048.js b/priv/static/static/js/app.a65abd01bcd13a691048.js new file mode 100644 index 000000000..2cb7ab18a Binary files /dev/null and b/priv/static/static/js/app.a65abd01bcd13a691048.js differ diff --git a/priv/static/static/js/app.a65abd01bcd13a691048.js.map b/priv/static/static/js/app.a65abd01bcd13a691048.js.map new file mode 100644 index 000000000..13c111497 Binary files /dev/null and b/priv/static/static/js/app.a65abd01bcd13a691048.js.map differ diff --git a/priv/static/static/js/app.d429b9c3b1c1f499710c.js b/priv/static/static/js/app.d429b9c3b1c1f499710c.js deleted file mode 100644 index c9fbdbc86..000000000 Binary files a/priv/static/static/js/app.d429b9c3b1c1f499710c.js and /dev/null differ diff --git a/priv/static/static/js/app.d429b9c3b1c1f499710c.js.map b/priv/static/static/js/app.d429b9c3b1c1f499710c.js.map deleted file mode 100644 index 677d7a6d2..000000000 Binary files a/priv/static/static/js/app.d429b9c3b1c1f499710c.js.map and /dev/null differ diff --git a/priv/static/static/js/manifest.0a010c91e43b948ff1a3.js b/priv/static/static/js/manifest.0a010c91e43b948ff1a3.js deleted file mode 100644 index 2c4c94961..000000000 Binary files a/priv/static/static/js/manifest.0a010c91e43b948ff1a3.js and /dev/null differ diff --git a/priv/static/static/js/manifest.35cf9744b80c38efa9fa.js b/priv/static/static/js/manifest.35cf9744b80c38efa9fa.js new file mode 100644 index 000000000..e9f02ce1f Binary files /dev/null and b/priv/static/static/js/manifest.35cf9744b80c38efa9fa.js differ diff --git a/priv/static/static/js/manifest.0a010c91e43b948ff1a3.js.map b/priv/static/static/js/manifest.35cf9744b80c38efa9fa.js.map similarity index 92% rename from priv/static/static/js/manifest.0a010c91e43b948ff1a3.js.map rename to priv/static/static/js/manifest.35cf9744b80c38efa9fa.js.map index 816274800..54dc2e253 100644 Binary files a/priv/static/static/js/manifest.0a010c91e43b948ff1a3.js.map and b/priv/static/static/js/manifest.35cf9744b80c38efa9fa.js.map differ diff --git a/priv/static/static/js/vendor.865340b21f539d34e2e1.js b/priv/static/static/js/vendor.7e4a5b87ce584522089d.js similarity index 65% rename from priv/static/static/js/vendor.865340b21f539d34e2e1.js rename to priv/static/static/js/vendor.7e4a5b87ce584522089d.js index 4a12a1d8c..fb693610e 100644 Binary files a/priv/static/static/js/vendor.865340b21f539d34e2e1.js and b/priv/static/static/js/vendor.7e4a5b87ce584522089d.js differ diff --git a/priv/static/static/js/vendor.7e4a5b87ce584522089d.js.map b/priv/static/static/js/vendor.7e4a5b87ce584522089d.js.map new file mode 100644 index 000000000..cb9e7fa37 Binary files /dev/null and b/priv/static/static/js/vendor.7e4a5b87ce584522089d.js.map differ diff --git a/priv/static/static/js/vendor.865340b21f539d34e2e1.js.map b/priv/static/static/js/vendor.865340b21f539d34e2e1.js.map deleted file mode 100644 index 71349dbb6..000000000 Binary files a/priv/static/static/js/vendor.865340b21f539d34e2e1.js.map and /dev/null differ diff --git a/priv/static/static/timeago-ca.json b/priv/static/static/timeago-ca.json new file mode 100644 index 000000000..ef782caf9 --- /dev/null +++ b/priv/static/static/timeago-ca.json @@ -0,0 +1,10 @@ +[ + "ara mateix", + ["fa %s s", "fa %s s"], + ["fa %s min", "fa %s min"], + ["fa %s h", "fa %s h"], + ["fa %s dia", "fa %s dies"], + ["fa %s setm.", "fa %s setm."], + ["fa %s mes", "fa %s mesos"], + ["fa %s any", "fa %s anys"] +] diff --git a/priv/static/static/timeago-ga.json b/priv/static/static/timeago-ga.json new file mode 100644 index 000000000..bdb7b6c42 --- /dev/null +++ b/priv/static/static/timeago-ga.json @@ -0,0 +1,10 @@ +[ + "Anois", + ["%s s", "%s s"], + ["%s n", "%s nóimeád"], + ["%s u", "%s uair"], + ["%s l", "%s lá"], + ["%s se", "%s seachtaine"], + ["%s m", "%s mí"], + ["%s b", "%s bliainta"] +] \ No newline at end of file diff --git a/priv/static/static/timeago-oc.json b/priv/static/static/timeago-oc.json new file mode 100644 index 000000000..a6b3932fb --- /dev/null +++ b/priv/static/static/timeago-oc.json @@ -0,0 +1,10 @@ +[ + "ara meteis", + ["fa %s s", "fa %s s"], + ["fa %s min", "fa %s min"], + ["fa %s h", "fa %s h"], + ["fa %s jorn", "fa %s jorns"], + ["fa %s setm.", "fa %s setm."], + ["fa %s mes", "fa %s meses"], + ["fa %s an", "fa %s ans"] +] diff --git a/test/config_test.exs b/test/config_test.exs new file mode 100644 index 000000000..0124544c8 --- /dev/null +++ b/test/config_test.exs @@ -0,0 +1,56 @@ +defmodule Pleroma.ConfigTest do + use ExUnit.Case + + test "get/1 with an atom" do + assert Pleroma.Config.get(:instance) == Application.get_env(:pleroma, :instance) + assert Pleroma.Config.get(:azertyuiop) == nil + assert Pleroma.Config.get(:azertyuiop, true) == true + end + + test "get/1 with a list of keys" do + assert Pleroma.Config.get([:instance, :public]) == + Keyword.get(Application.get_env(:pleroma, :instance), :public) + + assert Pleroma.Config.get([Pleroma.Web.Endpoint, :render_errors, :view]) == + get_in( + Application.get_env( + :pleroma, + Pleroma.Web.Endpoint + ), + [:render_errors, :view] + ) + + assert Pleroma.Config.get([:azerty, :uiop]) == nil + assert Pleroma.Config.get([:azerty, :uiop], true) == true + end + + test "get!/1" do + assert Pleroma.Config.get!(:instance) == Application.get_env(:pleroma, :instance) + + assert Pleroma.Config.get!([:instance, :public]) == + Keyword.get(Application.get_env(:pleroma, :instance), :public) + + assert_raise(Pleroma.Config.Error, fn -> + Pleroma.Config.get!(:azertyuiop) + end) + + assert_raise(Pleroma.Config.Error, fn -> + Pleroma.Config.get!([:azerty, :uiop]) + end) + end + + test "put/2 with a key" do + Pleroma.Config.put(:config_test, true) + + assert Pleroma.Config.get(:config_test) == true + end + + test "put/2 with a list of keys" do + Pleroma.Config.put([:instance, :config_test], true) + Pleroma.Config.put([:instance, :config_nested_test], []) + Pleroma.Config.put([:instance, :config_nested_test, :x], true) + + assert Pleroma.Config.get([:instance, :config_test]) == true + assert Pleroma.Config.get([:instance, :config_nested_test, :x]) == true + end +end diff --git a/test/filter_test.exs b/test/filter_test.exs index d81c92f08..509c15317 100644 --- a/test/filter_test.exs +++ b/test/filter_test.exs @@ -5,19 +5,88 @@ defmodule Pleroma.FilterTest do import Pleroma.Factory import Ecto.Query - test "creating a filter" do - user = insert(:user) + describe "creating filters" do + test "creating one filter" do + user = insert(:user) - query = %Pleroma.Filter{ - user_id: user.id, - filter_id: 42, - phrase: "knights", - context: ["home"] - } + query = %Pleroma.Filter{ + user_id: user.id, + filter_id: 42, + phrase: "knights", + context: ["home"] + } - {:ok, %Pleroma.Filter{} = filter} = Pleroma.Filter.create(query) - result = Pleroma.Filter.get(filter.filter_id, user) - assert query.phrase == result.phrase + {:ok, %Pleroma.Filter{} = filter} = Pleroma.Filter.create(query) + result = Pleroma.Filter.get(filter.filter_id, user) + assert query.phrase == result.phrase + end + + test "creating one filter without a pre-defined filter_id" do + user = insert(:user) + + query = %Pleroma.Filter{ + user_id: user.id, + phrase: "knights", + context: ["home"] + } + + {:ok, %Pleroma.Filter{} = filter} = Pleroma.Filter.create(query) + # Should start at 1 + assert filter.filter_id == 1 + end + + test "creating additional filters uses previous highest filter_id + 1" do + user = insert(:user) + + query_one = %Pleroma.Filter{ + user_id: user.id, + filter_id: 42, + phrase: "knights", + context: ["home"] + } + + {:ok, %Pleroma.Filter{} = filter_one} = Pleroma.Filter.create(query_one) + + query_two = %Pleroma.Filter{ + user_id: user.id, + # No filter_id + phrase: "who", + context: ["home"] + } + + {:ok, %Pleroma.Filter{} = filter_two} = Pleroma.Filter.create(query_two) + assert filter_two.filter_id == filter_one.filter_id + 1 + end + + test "filter_id is unique per user" do + user_one = insert(:user) + user_two = insert(:user) + + query_one = %Pleroma.Filter{ + user_id: user_one.id, + phrase: "knights", + context: ["home"] + } + + {:ok, %Pleroma.Filter{} = filter_one} = Pleroma.Filter.create(query_one) + + query_two = %Pleroma.Filter{ + user_id: user_two.id, + phrase: "who", + context: ["home"] + } + + {:ok, %Pleroma.Filter{} = filter_two} = Pleroma.Filter.create(query_two) + + assert filter_one.filter_id == 1 + assert filter_two.filter_id == 1 + + result_one = Pleroma.Filter.get(filter_one.filter_id, user_one) + assert result_one.phrase == filter_one.phrase + + result_two = Pleroma.Filter.get(filter_two.filter_id, user_two) + assert result_two.phrase == filter_two.phrase + end end test "deleting a filter" do diff --git a/test/fixtures/httpoison_mock/https___prismo.news__mxb.json b/test/fixtures/httpoison_mock/https___prismo.news__mxb.json new file mode 100644 index 000000000..a2fe53117 --- /dev/null +++ b/test/fixtures/httpoison_mock/https___prismo.news__mxb.json @@ -0,0 +1 @@ +{"id":"https://prismo.news/@mxb","type":"Person","name":"mxb","preferredUsername":"mxb","summary":"Creator of △ Prismo\r\n\r\nFollow me at @mb@mstdn.io","inbox":"https://prismo.news/ap/accounts/mxb/inbox","outbox":"https://prismo.news/ap/accounts/mxb/outbox","url":"https://prismo.news/@mxb","publicKey":{"id":"https://prismo.news/@mxb#main-key","owner":"https://prismo.news/@mxb","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA41gqLkBYuPLurC2TarF8\nbdyvqP54XzKyScJ6iPNkk4D4plYdWUVj0aOIHQ8LVfBeziH83jDMpRegm1sRLpNG\n1Ti+SzlWyTwugJ8wfQvwJL7iEzqhuPFddjPLpv0djMptvm5vtG6u6O3g4RpX12bv\n4pYRoMStPSv9KRKD/8Naw5Nv85PIWRc9rOly/EoVZBnbesroo69caiGthgChE2pa\niisQ5CEgj/615WUlUATkz3VdExKQkQOdeVABheIvcS5OsMurXnpWyLQ4n9WalNvF\nlJc08aOTIo4plsLAvdcGRDsBzio4qPok3jgzPpFkDqe+02WG/QMPT9VrzKO49N5R\nqQIDAQAB\n-----END PUBLIC KEY-----\n"},"icon":{"type":"Image","url":"https://prismo.s3.wasabisys.com/account/1/avatar/size_400-b6e570850878684362ba3b4bd9ceb007.jpg","media_type":null},"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"Hashtag":"as:Hashtag"},{"votes":{"@id":"as:votes","@type":"@id"}}]} \ No newline at end of file diff --git a/test/fixtures/prismo-url-map.json b/test/fixtures/prismo-url-map.json new file mode 100644 index 000000000..4e2e2fd4a --- /dev/null +++ b/test/fixtures/prismo-url-map.json @@ -0,0 +1,65 @@ +{ + "id": "https://prismo.news/posts/83#Create", + "type": "Create", + "actor": [ + { + "type": "Person", + "id": "https://prismo.news/@mxb" + } + ], + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "id": "https://prismo.news/posts/83", + "type": "Article", + "name": "Introducing: Federated follows!", + "published": "2018-11-01T07:10:05Z", + "content": "We are more than thrilled to announce that Prismo now supports federated follows! It means you ca...", + "url": { + "type": "Link", + "mimeType": "text/html", + "href": "https://prismo.news/posts/83" + }, + "votes": 12, + "attributedTo": [ + { + "type": "Person", + "id": "https://prismo.news/@mxb" + } + ], + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "tags": [ + { + "type": "Hashtag", + "href": "https://prismo.news/tags/prismo", + "name": "#prismo" + }, + { + "type": "Hashtag", + "href": "https://prismo.news/tags/prismodev", + "name": "#prismodev" + }, + { + "type": "Hashtag", + "href": "https://prismo.news/tags/meta", + "name": "#meta" + } + ], + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Hashtag": "as:Hashtag" + }, + { + "votes": { + "@id": "as:votes", + "@type": "@id" + } + } + ] + } +} diff --git a/test/notification_test.exs b/test/notification_test.exs index d86b5c1ab..a36ed5bb8 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -3,6 +3,7 @@ defmodule Pleroma.NotificationTest do alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.CommonAPI alias Pleroma.{User, Notification} + alias Pleroma.Web.ActivityPub.Transmogrifier import Pleroma.Factory describe "create_notifications" do @@ -121,6 +122,135 @@ test "it clears all notifications belonging to the user" do end end + describe "set_read_up_to()" do + test "it sets all notifications as read up to a specified notification ID" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + TwitterAPI.create_status(user, %{ + "status" => "hey @#{other_user.nickname}!" + }) + + {:ok, activity} = + TwitterAPI.create_status(user, %{ + "status" => "hey again @#{other_user.nickname}!" + }) + + [n2, n1] = notifs = Notification.for_user(other_user) + assert length(notifs) == 2 + + assert n2.id > n1.id + + {:ok, activity} = + TwitterAPI.create_status(user, %{ + "status" => "hey yet again @#{other_user.nickname}!" + }) + + Notification.set_read_up_to(other_user, n2.id) + + [n3, n2, n1] = notifs = Notification.for_user(other_user) + + assert n1.seen == true + assert n2.seen == true + assert n3.seen == false + end + end + + describe "notification target determination" do + test "it sends notifications to addressed users in new messages" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "hey @#{other_user.nickname}!" + }) + + assert other_user in Notification.get_notified_from_activity(activity) + end + + test "it sends notifications to mentioned users in new messages" do + user = insert(:user) + other_user = insert(:user) + + create_activity = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "actor" => user.ap_id, + "object" => %{ + "type" => "Note", + "content" => "message with a Mention tag, but no explicit tagging", + "tag" => [ + %{ + "type" => "Mention", + "href" => other_user.ap_id, + "name" => other_user.nickname + } + ], + "attributedTo" => user.ap_id + } + } + + {:ok, activity} = Transmogrifier.handle_incoming(create_activity) + + assert other_user in Notification.get_notified_from_activity(activity) + end + + test "it does not send notifications to users who are only cc in new messages" do + user = insert(:user) + other_user = insert(:user) + + create_activity = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [other_user.ap_id], + "actor" => user.ap_id, + "object" => %{ + "type" => "Note", + "content" => "hi everyone", + "attributedTo" => user.ap_id + } + } + + {:ok, activity} = Transmogrifier.handle_incoming(create_activity) + + assert other_user not in Notification.get_notified_from_activity(activity) + end + + test "it does not send notification to mentioned users in likes" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity_one} = + CommonAPI.post(user, %{ + "status" => "hey @#{other_user.nickname}!" + }) + + {:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, third_user) + + assert other_user not in Notification.get_notified_from_activity(activity_two) + end + + test "it does not send notification to mentioned users in announces" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity_one} = + CommonAPI.post(user, %{ + "status" => "hey @#{other_user.nickname}!" + }) + + {:ok, activity_two, _} = CommonAPI.repeat(activity_one.id, third_user) + + assert other_user not in Notification.get_notified_from_activity(activity_two) + end + end + describe "notification lifecycle" do test "liking an activity results in 1 notification, then 0 if the activity is deleted" do user = insert(:user) diff --git a/test/object_test.exs b/test/object_test.exs index 5eb9b7530..909605560 100644 --- a/test/object_test.exs +++ b/test/object_test.exs @@ -19,4 +19,34 @@ test "it ensures uniqueness of the id" do {:error, _result} = Repo.insert(cs) end end + + describe "deletion function" do + test "deletes an object" do + object = insert(:note) + found_object = Object.get_by_ap_id(object.data["id"]) + + assert object == found_object + + Object.delete(found_object) + + found_object = Object.get_by_ap_id(object.data["id"]) + + refute object == found_object + end + + test "ensures cache is cleared for the object" do + object = insert(:note) + cached_object = Object.get_cached_by_ap_id(object.data["id"]) + + assert object == cached_object + + Object.delete(cached_object) + + {:ok, nil} = Cachex.get(:object_cache, "object:#{object.data["id"]}") + + cached_object = Object.get_cached_by_ap_id(object.data["id"]) + + refute object == cached_object + end + end end diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs new file mode 100644 index 000000000..55040a108 --- /dev/null +++ b/test/plugs/http_security_plug_test.exs @@ -0,0 +1,77 @@ +defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do + use Pleroma.Web.ConnCase + alias Pleroma.Config + alias Plug.Conn + + test "it sends CSP headers when enabled", %{conn: conn} do + Config.put([:http_security, :enabled], true) + + conn = + conn + |> get("/api/v1/instance") + + refute Conn.get_resp_header(conn, "x-xss-protection") == [] + refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] + refute Conn.get_resp_header(conn, "x-frame-options") == [] + refute Conn.get_resp_header(conn, "x-content-type-options") == [] + refute Conn.get_resp_header(conn, "x-download-options") == [] + refute Conn.get_resp_header(conn, "referrer-policy") == [] + refute Conn.get_resp_header(conn, "content-security-policy") == [] + end + + test "it does not send CSP headers when disabled", %{conn: conn} do + Config.put([:http_security, :enabled], false) + + conn = + conn + |> get("/api/v1/instance") + + assert Conn.get_resp_header(conn, "x-xss-protection") == [] + assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == [] + assert Conn.get_resp_header(conn, "x-frame-options") == [] + assert Conn.get_resp_header(conn, "x-content-type-options") == [] + assert Conn.get_resp_header(conn, "x-download-options") == [] + assert Conn.get_resp_header(conn, "referrer-policy") == [] + assert Conn.get_resp_header(conn, "content-security-policy") == [] + end + + test "it sends STS headers when enabled", %{conn: conn} do + Config.put([:http_security, :enabled], true) + Config.put([:http_security, :sts], true) + + conn = + conn + |> get("/api/v1/instance") + + refute Conn.get_resp_header(conn, "strict-transport-security") == [] + refute Conn.get_resp_header(conn, "expect-ct") == [] + end + + test "it does not send STS headers when disabled", %{conn: conn} do + Config.put([:http_security, :enabled], true) + Config.put([:http_security, :sts], false) + + conn = + conn + |> get("/api/v1/instance") + + assert Conn.get_resp_header(conn, "strict-transport-security") == [] + assert Conn.get_resp_header(conn, "expect-ct") == [] + end + + test "referrer-policy header reflects configured value", %{conn: conn} do + conn = + conn + |> get("/api/v1/instance") + + assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"] + + Config.put([:http_security, :referrer_policy], "no-referrer") + + conn = + build_conn() + |> get("/api/v1/instance") + + assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"] + end +end diff --git a/test/support/httpoison_mock.ex b/test/support/httpoison_mock.ex index 75c78d70e..ab964334d 100644 --- a/test/support/httpoison_mock.ex +++ b/test/support/httpoison_mock.ex @@ -3,6 +3,14 @@ defmodule HTTPoisonMock do def get(url, body \\ [], headers \\ []) + def get("https://prismo.news/@mxb", _, _) do + {:ok, + %Response{ + status_code: 200, + body: File.read!("test/fixtures/httpoison_mock/https___prismo.news__mxb.json") + }} + end + def get("https://osada.macgirvin.com/channel/mike", _, _) do {:ok, %Response{ diff --git a/test/user_test.exs b/test/user_test.exs index 248c26a3d..7dec3462f 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -487,11 +487,13 @@ test "get recipients from activity" do assert addressed in recipients end - test ".deactivate deactivates a user" do + test ".deactivate can de-activate then re-activate a user" do user = insert(:user) assert false == !!user.info["deactivated"] {:ok, user} = User.deactivate(user) assert true == user.info["deactivated"] + {:ok, user} = User.deactivate(user, false) + assert false == !!user.info["deactivated"] end test ".delete deactivates a user, all follow relationships and all create activities" do @@ -509,7 +511,7 @@ test ".delete deactivates a user, all follow relationships and all create activi {:ok, _, _} = CommonAPI.favorite(activity.id, follower) {:ok, _, _} = CommonAPI.repeat(activity.id, follower) - :ok = User.delete(user) + {:ok, _} = User.delete(user) followed = Repo.get(User, followed.id) follower = Repo.get(User, follower.id) @@ -549,4 +551,31 @@ test "html_filter_policy returns TwitterText scrubber when rich-text is disabled assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user) end end + + describe "caching" do + test "invalidate_cache works" do + user = insert(:user) + user_info = User.get_cached_user_info(user) + + User.invalidate_cache(user) + + {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") + {:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}") + {:ok, nil} = Cachex.get(:user_cache, "user_info:#{user.id}") + end + + test "User.delete() plugs any possible zombie objects" do + user = insert(:user) + + {:ok, _} = User.delete(user) + + {:ok, cached_user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") + + assert cached_user != user + + {:ok, cached_user} = Cachex.get(:user_cache, "nickname:#{user.ap_id}") + + assert cached_user != user + end + end end diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index e63cd6583..1c24b348c 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -5,6 +5,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.{Repo, User} alias Pleroma.Activity + describe "/relay" do + test "with the relay active, it returns the relay user", %{conn: conn} do + res = + conn + |> get(activity_pub_path(conn, :relay)) + |> json_response(200) + + assert res["id"] =~ "/relay" + end + + test "with the relay disabled, it returns 404", %{conn: conn} do + Pleroma.Config.put([:instance, :allow_relay], false) + + res = + conn + |> get(activity_pub_path(conn, :relay)) + |> json_response(404) + + Pleroma.Config.put([:instance, :allow_relay], true) + end + end + describe "/users/:nickname" do test "it returns a json representation of the user", %{conn: conn} do user = insert(:user) diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs new file mode 100644 index 000000000..41d13e055 --- /dev/null +++ b/test/web/activity_pub/relay_test.exs @@ -0,0 +1,11 @@ +defmodule Pleroma.Web.ActivityPub.RelayTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Relay + + test "gets an actor for the relay" do + user = Relay.get_actor() + + assert user.ap_id =~ "/relay" + end +end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 6a6f2a44c..0278ef5d1 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -145,6 +145,14 @@ test "it works for incoming notices with tag not being an array (kroeg)" do assert "test" in data["object"]["tag"] end + test "it works for incoming notices with url not being a string (prismo)" do + data = File.read!("test/fixtures/prismo-url-map.json") |> Poison.decode!() + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["object"]["url"] == "https://prismo.news/posts/83" + end + test "it works for incoming follow requests" do user = insert(:user) @@ -695,7 +703,9 @@ test "it adds the json-ld context and the conversation property" do {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"}) {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - assert modified["@context"] == "https://www.w3.org/ns/activitystreams" + assert modified["@context"] == + Pleroma.Web.ActivityPub.Utils.make_json_ld_header()["@context"] + assert modified["object"]["conversation"] == modified["context"] end @@ -733,6 +743,39 @@ test "it translates ostatus reply_to IDs to external URLs" do assert modified["object"]["inReplyTo"] == "http://gs.example.org:4040/index.php/notice/29" end + + test "it strips internal hashtag data" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu"}) + + expected_tag = %{ + "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu", + "type" => "Hashtag", + "name" => "#2hu" + } + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["object"]["tag"] == [expected_tag] + end + + test "it strips internal fields" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu :moominmamma:"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert length(modified["object"]["tag"]) == 2 + + assert is_nil(modified["object"]["emoji"]) + assert is_nil(modified["object"]["likes"]) + assert is_nil(modified["object"]["like_count"]) + assert is_nil(modified["object"]["announcements"]) + assert is_nil(modified["object"]["announcement_count"]) + assert is_nil(modified["object"]["context_id"]) + end end describe "user upgrade" do diff --git a/test/web/activity_pub/views/object_view_test.exs b/test/web/activity_pub/views/object_view_test.exs index 6a1311be7..7e08dff5d 100644 --- a/test/web/activity_pub/views/object_view_test.exs +++ b/test/web/activity_pub/views/object_view_test.exs @@ -13,5 +13,6 @@ test "renders a note object" do assert result["to"] == note.data["to"] assert result["content"] == note.data["content"] assert result["type"] == "Note" + assert result["@context"] end end diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs index 09533362a..c709d1181 100644 --- a/test/web/federator_test.exs +++ b/test/web/federator_test.exs @@ -1,6 +1,9 @@ defmodule Pleroma.Web.FederatorTest do alias Pleroma.Web.Federator + alias Pleroma.Web.CommonAPI use Pleroma.DataCase + import Pleroma.Factory + import Mock test "enqueues an element according to priority" do queue = [%{item: 1, priority: 2}] @@ -17,4 +20,45 @@ test "pop first item" do assert {2, [%{item: 1, priority: 2}]} = Federator.queue_pop(queue) end + + describe "Publish an activity" do + setup do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "HI"}) + + relay_mock = { + Pleroma.Web.ActivityPub.Relay, + [], + [publish: fn _activity -> send(self(), :relay_publish) end] + } + + %{activity: activity, relay_mock: relay_mock} + end + + test "with relays active, it publishes to the relay", %{ + activity: activity, + relay_mock: relay_mock + } do + with_mocks([relay_mock]) do + Federator.handle(:publish, activity) + end + + assert_received :relay_publish + end + + test "with relays deactivated, it does not publish to the relay", %{ + activity: activity, + relay_mock: relay_mock + } do + Pleroma.Config.put([:instance, :allow_relay], false) + + with_mocks([relay_mock]) do + Federator.handle(:publish, activity) + end + + refute_received :relay_publish + + Pleroma.Config.put([:instance, :allow_relay], true) + end + end end diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index e9deae64d..ad67cae6b 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -178,6 +178,32 @@ test "direct timeline", %{conn: conn} do |> get("api/v1/timelines/home") [_s1, _s2] = json_response(res_conn, 200) + + # Test pagination + Enum.each(1..20, fn _ -> + {:ok, _} = + CommonAPI.post(user_one, %{ + "status" => "Hi @#{user_two.nickname}!", + "visibility" => "direct" + }) + end) + + res_conn = + conn + |> assign(:user, user_two) + |> get("api/v1/timelines/direct") + + statuses = json_response(res_conn, 200) + assert length(statuses) == 20 + + res_conn = + conn + |> assign(:user, user_two) + |> get("api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]}) + + [status] = json_response(res_conn, 200) + + assert status["url"] != direct.data["id"] end test "replying to a status", %{conn: conn} do @@ -198,6 +224,21 @@ test "replying to a status", %{conn: conn} do assert activity.data["object"]["inReplyToStatusId"] == replied_to.id end + test "posting a status with an invalid in_reply_to_id", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""}) + + assert %{"content" => "xD", "id" => id} = json_response(conn, 200) + + activity = Repo.get(Activity, id) + + assert activity + end + test "verify_credentials", %{conn: conn} do user = insert(:user) @@ -280,6 +321,8 @@ test "creating a filter", %{conn: conn} do assert response = json_response(conn, 200) assert response["phrase"] == filter.phrase assert response["context"] == filter.context + assert response["id"] != nil + assert response["id"] != "" end test "fetching a list of filters", %{conn: conn} do @@ -927,11 +970,20 @@ test "hashtag timeline", %{conn: conn} do {:ok, [_activity]} = OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") - conn = + nconn = conn |> get("/api/v1/timelines/tag/2hu") - assert [%{"id" => id}] = json_response(conn, 200) + assert [%{"id" => id}] = json_response(nconn, 200) + + assert id == to_string(activity.id) + + # works for different capitalization too + nconn = + conn + |> get("/api/v1/timelines/tag/2HU") + + assert [%{"id" => id}] = json_response(nconn, 200) assert id == to_string(activity.id) end) diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index b9c019206..31554a07d 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -7,6 +7,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do alias Pleroma.Web.CommonAPI import Pleroma.Factory + test "a note with null content" do + note = insert(:note_activity) + + data = + note.data + |> put_in(["object", "content"], nil) + + note = + note + |> Map.put(:data, data) + + user = User.get_cached_by_ap_id(note.data["actor"]) + + status = StatusView.render("status.json", %{activity: note}) + + assert status.content == "" + end + test "a note activity" do note = insert(:note_activity) user = User.get_cached_by_ap_id(note.data["actor"]) diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs index d48f40e47..a6376453c 100644 --- a/test/web/node_info_test.exs +++ b/test/web/node_info_test.exs @@ -14,4 +14,36 @@ test "nodeinfo shows staff accounts", %{conn: conn} do assert user.ap_id in result["metadata"]["staffAccounts"] end + + test "returns 404 when federation is disabled" do + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:federating, false) + + Application.put_env(:pleroma, :instance, instance) + + conn + |> get("/.well-known/nodeinfo") + |> json_response(404) + + conn + |> get("/nodeinfo/2.0.json") + |> json_response(404) + + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:federating, true) + + Application.put_env(:pleroma, :instance, instance) + end + + test "returns 200 when federation is enabled" do + conn + |> get("/.well-known/nodeinfo") + |> json_response(200) + + conn + |> get("/nodeinfo/2.0.json") + |> json_response(200) + end end diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs index f095e41dd..f95da8b0a 100644 --- a/test/web/ostatus/ostatus_test.exs +++ b/test/web/ostatus/ostatus_test.exs @@ -456,4 +456,28 @@ test "it doesn't add nil in the do field" do "https://www.w3.org/ns/activitystreams#Public" ] end + + describe "is_representable?" do + test "Note objects are representable" do + note_activity = insert(:note_activity) + + assert OStatus.is_representable?(note_activity) + end + + test "Article objects are not representable" do + note_activity = insert(:note_activity) + + note_object = Object.normalize(note_activity.data["object"]) + + note_data = + note_object.data + |> Map.put("type", "Article") + + cs = Object.change(note_object, %{data: note_data}) + {:ok, article_object} = Repo.update(cs) + + # the underlying object is now an Article instead of a note, so this should fail + refute OStatus.is_representable?(note_activity) + end + end end diff --git a/test/web/plugs/federating_plug_test.exs b/test/web/plugs/federating_plug_test.exs new file mode 100644 index 000000000..1455a1c46 --- /dev/null +++ b/test/web/plugs/federating_plug_test.exs @@ -0,0 +1,33 @@ +defmodule Pleroma.Web.FederatingPlugTest do + use Pleroma.Web.ConnCase + + test "returns and halt the conn when federating is disabled" do + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:federating, false) + + Application.put_env(:pleroma, :instance, instance) + + conn = + build_conn() + |> Pleroma.Web.FederatingPlug.call(%{}) + + assert conn.status == 404 + assert conn.halted + + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:federating, true) + + Application.put_env(:pleroma, :instance, instance) + end + + test "does nothing when federating is enabled" do + conn = + build_conn() + |> Pleroma.Web.FederatingPlug.call(%{}) + + refute conn.status + refute conn.halted + end +end diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs index 87bcdaf71..788e3a6eb 100644 --- a/test/web/twitter_api/twitter_api_controller_test.exs +++ b/test/web/twitter_api/twitter_api_controller_test.exs @@ -100,6 +100,56 @@ test "returns statuses", %{conn: conn} do assert length(response) == 10 end + + test "returns 403 to unauthenticated request when the instance is not public" do + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, false) + + Application.put_env(:pleroma, :instance, instance) + + conn + |> get("/api/statuses/public_timeline.json") + |> json_response(403) + + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, true) + + Application.put_env(:pleroma, :instance, instance) + end + + test "returns 200 to unauthenticated request when the instance is public" do + conn + |> get("/api/statuses/public_timeline.json") + |> json_response(200) + end + end + + describe "GET /statuses/public_and_external_timeline.json" do + test "returns 403 to unauthenticated request when the instance is not public" do + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, false) + + Application.put_env(:pleroma, :instance, instance) + + conn + |> get("/api/statuses/public_and_external_timeline.json") + |> json_response(403) + + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, true) + + Application.put_env(:pleroma, :instance, instance) + end + + test "returns 200 to unauthenticated request when the instance is public" do + conn + |> get("/api/statuses/public_and_external_timeline.json") + |> json_response(200) + end end describe "GET /statuses/show/:id.json" do @@ -221,6 +271,36 @@ test "with credentials", %{conn: conn, user: current_user} do end end + describe "GET /statuses/dm_timeline.json" do + test "it show direct messages", %{conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + + {:ok, user_two} = User.follow(user_two, user_one) + + {:ok, direct} = + CommonAPI.post(user_one, %{ + "status" => "Hi @#{user_two.nickname}!", + "visibility" => "direct" + }) + + {:ok, _follower_only} = + CommonAPI.post(user_one, %{ + "status" => "Hi @#{user_two.nickname}!", + "visibility" => "private" + }) + + # Only direct should be visible here + res_conn = + conn + |> assign(:user, user_two) + |> get("/api/statuses/dm_timeline.json") + + [status] = json_response(res_conn, 200) + assert status["id"] == direct.id + end + end + describe "GET /statuses/mentions.json" do setup [:valid_user] @@ -281,6 +361,56 @@ test "with credentials", %{conn: conn, user: current_user} do end end + describe "POST /api/qvitter/statuses/notifications/read" do + setup [:valid_user] + + test "without valid credentials", %{conn: conn} do + conn = post(conn, "/api/qvitter/statuses/notifications/read", %{"latest_id" => 1_234_567}) + assert json_response(conn, 403) == %{"error" => "Invalid credentials."} + end + + test "with credentials, without any params", %{conn: conn, user: current_user} do + conn = + conn + |> with_credentials(current_user.nickname, "test") + |> post("/api/qvitter/statuses/notifications/read") + + assert json_response(conn, 400) == %{ + "error" => "You need to specify latest_id", + "request" => "/api/qvitter/statuses/notifications/read" + } + end + + test "with credentials, with params", %{conn: conn, user: current_user} do + other_user = insert(:user) + + {:ok, _activity} = + ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user}) + + response_conn = + conn + |> with_credentials(current_user.nickname, "test") + |> get("/api/qvitter/statuses/notifications.json") + + [notification] = response = json_response(response_conn, 200) + + assert length(response) == 1 + + assert notification["is_seen"] == 0 + + response_conn = + conn + |> with_credentials(current_user.nickname, "test") + |> post("/api/qvitter/statuses/notifications/read", %{"latest_id" => notification["id"]}) + + [notification] = response = json_response(response_conn, 200) + + assert length(response) == 1 + + assert notification["is_seen"] == 1 + end + end + describe "GET /statuses/user_timeline.json" do setup [:valid_user] diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs index 6486540f8..8b9920bd9 100644 --- a/test/web/twitter_api/twitter_api_test.exs +++ b/test/web/twitter_api/twitter_api_test.exs @@ -48,7 +48,7 @@ test "create a status" do "https://www.w3.org/ns/activitystreams#Public" ) - assert Enum.member?(get_in(activity.data, ["cc"]), "shp") + assert Enum.member?(get_in(activity.data, ["to"]), "shp") assert activity.local == true assert %{"moominmamma" => "http://localhost:4001/finmoji/128px/moominmamma-128.png"} = diff --git a/test/web/twitter_api/views/user_view_test.exs b/test/web/twitter_api/views/user_view_test.exs index 2deb22fb1..2c583c0d3 100644 --- a/test/web/twitter_api/views/user_view_test.exs +++ b/test/web/twitter_api/views/user_view_test.exs @@ -13,6 +13,13 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do [user: user] end + test "A user with only a nickname", %{user: user} do + user = %{user | name: nil, nickname: "scarlett@catgirl.science"} + represented = UserView.render("show.json", %{user: user}) + assert represented["name"] == user.nickname + assert represented["name_html"] == user.nickname + end + test "A user with an avatar object", %{user: user} do image = "image" user = %{user | avatar: %{"url" => [%{"href" => image}]}}