Merge remote-tracking branch 'official/develop' into develop

This commit is contained in:
Hakaba Hitoyo 2018-11-15 14:04:09 +09:00
commit 5c8b8f6cb7
111 changed files with 2422 additions and 817 deletions

View file

@ -1,4 +1,4 @@
image: elixir:1.6.4 image: elixir:1.7.2
services: services:
- postgres:9.6.2 - postgres:9.6.2

View file

@ -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

View file

@ -2,11 +2,13 @@
## About Pleroma ## 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). 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 * Twidere
* Tusky * Tusky
@ -15,6 +17,7 @@ Mobile clients that are known to work well:
* Amaroq (iOS) * Amaroq (iOS)
* Tootdon (Android + iOS) * Tootdon (Android + iOS)
* Tootle (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. 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. * 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. * 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 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. 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 ## 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. * 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. 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) ### 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` Running `service pleroma start`
Logs can be watched by using `journalctl -fu pleroma.service` 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 ### Standalone/run by other means
Run `mix phx.server` in repository's root, it will output log into stdout/stderr 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. This is useful for running pleroma inside Tor or i2p.
## Admin Tasks
### Register a User
Run `mix register_user <name> <nickname> <email> <bio> <password>`. 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 ## Troubleshooting
### No incoming federation ### No incoming federation

View file

@ -20,7 +20,8 @@
config :pleroma, Pleroma.Uploaders.S3, config :pleroma, Pleroma.Uploaders.S3,
bucket: nil, 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"] config :pleroma, :emoji, shortcode_globs: ["/emoji/custom/**/*.png"]
@ -84,6 +85,9 @@
description: "A Pleroma instance, an alternative fediverse server", description: "A Pleroma instance, an alternative fediverse server",
limit: 5000, limit: 5000,
upload_limit: 16_000_000, 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, registrations_open: true,
federating: true, federating: true,
allow_relay: true, allow_relay: true,
@ -172,6 +176,27 @@
limit: 23, limit: 23,
web: "https://vinayaka.distsn.org/?{{host}}+{{user}}" 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 # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

89
config/config.md Normal file
View file

@ -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 pleromas 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 instances name
* `email`: Email used to reach an Administrator/Moderator of the instance
* `description`: The instances 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 users profile avatars
* `background_upload_limit`: File size limit of users profile backgrounds
* `banner_upload_limit`: File size limit of users profile backgrounds
* `registerations_open`: Enable registerations for anyone, invitations can be used when false.
* `federating`
* `allow_relay`: Enable Pleromas 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`: Doesnt modify activities (default)
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesnt 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 Pleromas 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 isnt 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 instances 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 instances 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"`.

View file

@ -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 log /var/log/caddy/pleroma_access.log
errors /var/log/caddy/pleroma_error.log errors /var/log/caddy/pleroma_error.log
@ -9,34 +15,12 @@ social.domain.tld {
transparent transparent
} }
tls user@domain.tld { tls {
# Remove the rest of the lines in here, if you want to support older devices # Remove the rest of the lines in here, if you want to support older devices
key_type p256 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 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. # 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. # To use this directive, you need the http.cache plugin for Caddy.
cache { cache {

View file

@ -1,24 +1,31 @@
#Example configuration for when Apache httpd and Pleroma are on the same host. # default Apache site config for Pleroma
#Needed modules: headers proxy proxy_http proxy_wstunnel rewrite ssl #
#This assumes a Debian style Apache config. Put this in /etc/apache2/sites-available # needed modules: define headers proxy proxy_http proxy_wstunnel rewrite ssl
#Install your TLS certificate, possibly using Let's Encrypt. #
#Replace 'pleroma.example.com' with your instance's domain wherever it appears # 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 ServerTokens Prod
ErrorLog ${APACHE_LOG_DIR}/error.log ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined CustomLog ${APACHE_LOG_DIR}/access.log combined
<VirtualHost *:80> <VirtualHost *:80>
Redirect permanent / https://pleroma.example.com Redirect permanent / https://${servername}
</VirtualHost> </VirtualHost>
<VirtualHost *:443> <VirtualHost *:443>
SSLEngine on SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/pleroma.example.com/cert.pem SSLCertificateFile /etc/letsencrypt/live/${servername}/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/pleroma.example.com/privkey.pem SSLCertificateKeyFile /etc/letsencrypt/live/${servername}/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/pleroma.example.com/fullchain.pem SSLCertificateChainFile /etc/letsencrypt/live/${servername}/fullchain.pem
# Mozilla modern configuration, tweak to your needs # Mozilla modern configuration, tweak to your needs
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
@ -27,15 +34,6 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLCompression off SSLCompression off
SSLSessionTickets 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 RewriteEngine On
RewriteCond %{HTTP:Connection} Upgrade [NC] RewriteCond %{HTTP:Connection} Upgrade [NC]
RewriteCond %{HTTP:Upgrade} websocket [NC] RewriteCond %{HTTP:Upgrade} websocket [NC]
@ -45,7 +43,7 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
ProxyPass / http://localhost:4000/ ProxyPass / http://localhost:4000/
ProxyPassReverse / http://localhost:4000/ ProxyPassReverse / http://localhost:4000/
RequestHeader set Host "pleroma.example.com" RequestHeader set Host ${servername}
ProxyPreserveHost On ProxyPreserveHost On
</VirtualHost> </VirtualHost>

View file

@ -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; inactive=720m use_temp_path=off;
server { server {
listen 80;
server_name example.tld; server_name example.tld;
listen 80;
return 301 https://$server_name$request_uri; return 301 https://$server_name$request_uri;
# Uncomment this if you need to use the 'webroot' method with certbot. Make sure # Uncomment this if you need to use the 'webroot' method with certbot. Make sure
@ -60,28 +60,6 @@ server {
client_max_body_size 16m; client_max_body_size 16m;
location / { 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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";

View file

@ -6,6 +6,7 @@ After=network.target postgresql.service
User=pleroma User=pleroma
WorkingDirectory=/home/pleroma/pleroma WorkingDirectory=/home/pleroma/pleroma
Environment="HOME=/home/pleroma" Environment="HOME=/home/pleroma"
Environment="MIX_ENV=prod"
ExecStart=/usr/local/bin/mix phx.server ExecStart=/usr/local/bin/mix phx.server
ExecReload=/bin/kill $MAINPID ExecReload=/bin/kill $MAINPID
KillMode=process KillMode=process

View file

@ -119,13 +119,3 @@ sub vcl_pipe {
set bereq.http.connection = req.http.connection; 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";
}

View file

@ -2,7 +2,13 @@ defmodule Mix.Tasks.DeactivateUser do
use Mix.Task use Mix.Task
alias Pleroma.User alias Pleroma.User
@shortdoc "Toggle deactivation status for a user" @moduledoc """
Deactivates a user (local or remote)
Usage: ``mix deactivate_user <nickname>``
Example: ``mix deactivate_user lain``
"""
def run([nickname]) do def run([nickname]) do
Mix.Task.run("app.start") Mix.Task.run("app.start")

View file

@ -1,7 +1,15 @@
defmodule Mix.Tasks.GenerateConfig do defmodule Mix.Tasks.GenerateConfig do
use Mix.Task 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 def run(_) do
IO.puts("Answer a few questions to generate a new config\n") IO.puts("Answer a few questions to generate a new config\n")
IO.puts("--- THIS WILL OVERWRITE YOUR config/generated_config.exs! ---\n") IO.puts("--- THIS WILL OVERWRITE YOUR config/generated_config.exs! ---\n")

View file

@ -1,7 +1,14 @@
defmodule Mix.Tasks.GenerateInviteToken do defmodule Mix.Tasks.GenerateInviteToken do
use Mix.Task 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 def run([]) do
Mix.Task.run("app.start") Mix.Task.run("app.start")

View file

@ -2,7 +2,13 @@ defmodule Mix.Tasks.GeneratePasswordReset do
use Mix.Task use Mix.Task
alias Pleroma.User alias Pleroma.User
@shortdoc "Generate password reset link for user" @moduledoc """
Generate password reset link for user
Usage: ``mix generate_password_reset <nickname>``
Example: ``mix generate_password_reset lain``
"""
def run([nickname]) do def run([nickname]) do
Mix.Task.run("app.start") Mix.Task.run("app.start")

View file

@ -1,9 +1,16 @@
defmodule Mix.Tasks.SetModerator do defmodule Mix.Tasks.SetModerator do
@moduledoc """
Set moderator to a local user
Usage: ``mix set_moderator <nickname>``
Example: ``mix set_moderator lain``
"""
use Mix.Task use Mix.Task
import Mix.Ecto import Mix.Ecto
alias Pleroma.{Repo, User} alias Pleroma.{Repo, User}
@shortdoc "Set moderator status"
def run([nickname | rest]) do def run([nickname | rest]) do
Application.ensure_all_started(:pleroma) Application.ensure_all_started(:pleroma)

View file

@ -0,0 +1,19 @@
defmodule Mix.Tasks.ReactivateUser do
use Mix.Task
alias Pleroma.User
@moduledoc """
Reactivate a user
Usage: ``mix reactivate_user <nickname>``
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

View file

@ -1,4 +1,12 @@
defmodule Mix.Tasks.RegisterUser do defmodule Mix.Tasks.RegisterUser do
@moduledoc """
Manually register a local user
Usage: ``mix register_user <name> <nickname> <email> <bio> <password>``
Example: ``mix register_user lain lain@example.org "blushy-crushy fediverse idol + pleroma dev" pleaseDontHeckLain``
"""
use Mix.Task use Mix.Task
alias Pleroma.{Repo, User} alias Pleroma.{Repo, User}

View file

@ -4,6 +4,13 @@ defmodule Mix.Tasks.RelayFollow do
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
@shortdoc "Follows a remote relay" @shortdoc "Follows a remote relay"
@moduledoc """
Follows a remote relay
Usage: ``mix relay_follow <relay_url>``
Example: ``mix relay_follow https://example.org/relay``
"""
def run([target]) do def run([target]) do
Mix.Task.run("app.start") Mix.Task.run("app.start")

View file

@ -3,7 +3,13 @@ defmodule Mix.Tasks.RelayUnfollow do
require Logger require Logger
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
@shortdoc "Follows a remote relay" @moduledoc """
Unfollows a remote relay
Usage: ``mix relay_follow <relay_url>``
Example: ``mix relay_follow https://example.org/relay``
"""
def run([target]) do def run([target]) do
Mix.Task.run("app.start") Mix.Task.run("app.start")

View file

@ -2,12 +2,18 @@ defmodule Mix.Tasks.RmUser do
use Mix.Task use Mix.Task
alias Pleroma.User 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 def run([nickname]) do
Mix.Task.run("app.start") Mix.Task.run("app.start")
with %User{local: true} = user <- User.get_by_nickname(nickname) do with %User{local: true} = user <- User.get_by_nickname(nickname) do
User.delete(user) {:ok, _} = User.delete(user)
end end
end end
end end

View file

@ -25,6 +25,10 @@ config :pleroma, Pleroma.Repo,
hostname: "localhost", hostname: "localhost",
pool_size: 10 pool_size: 10
# Enable Strict-Transport-Security once SSL is working:
# config :pleroma, :http_security,
# sts: true
# Configure S3 support if desired. # Configure S3 support if desired.
# The public S3 endpoint is different depending on region and provider, # The public S3 endpoint is different depending on region and provider,
# consult your S3 provider's documentation for details on what to use. # consult your S3 provider's documentation for details on what to use.

View file

@ -1,8 +1,5 @@
CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB; CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>';
-- in case someone runs this second time accidentally CREATE DATABASE pleroma_dev OWNER pleroma;
ALTER USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB;
CREATE DATABASE pleroma_dev;
ALTER DATABASE pleroma_dev OWNER TO pleroma;
\c pleroma_dev; \c pleroma_dev;
--Extensions made by ecto.migrate that need superuser access --Extensions made by ecto.migrate that need superuser access
CREATE EXTENSION IF NOT EXISTS citext; CREATE EXTENSION IF NOT EXISTS citext;

View file

@ -1,9 +1,18 @@
defmodule Mix.Tasks.SetLocked do 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 <username>``
Example: ``mix set_locked lain``
"""
use Mix.Task use Mix.Task
import Mix.Ecto import Mix.Ecto
alias Pleroma.{Repo, User} alias Pleroma.{Repo, User}
@shortdoc "Set locked status"
def run([nickname | rest]) do def run([nickname | rest]) do
ensure_started(Repo, []) ensure_started(Repo, [])

View file

@ -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 <nickname>``
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

View file

@ -12,18 +12,35 @@ def start(_type, _args) do
[ [
# Start the Ecto repository # Start the Ecto repository
supervisor(Pleroma.Repo, []), supervisor(Pleroma.Repo, []),
worker(Pleroma.Emoji, []),
# Start the endpoint when the application starts # Start the endpoint when the application starts
supervisor(Pleroma.Web.Endpoint, []), supervisor(Pleroma.Web.Endpoint, []),
# Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3) # Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3)
# worker(Pleroma.Worker, [arg1, arg2, arg3]), # worker(Pleroma.Worker, [arg1, arg2, arg3]),
worker(Cachex, [ worker(
:user_cache, Cachex,
[ [
default_ttl: 25000, :user_cache,
ttl_interval: 1000, [
limit: 2500 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( worker(
Cachex, Cachex,
[ [
@ -40,8 +57,8 @@ def start(_type, _args) do
id: :cachex_idem id: :cachex_idem
), ),
worker(Pleroma.Web.Federator, []), worker(Pleroma.Web.Federator, []),
worker(Pleroma.Gopher.Server, []), worker(Pleroma.Stats, []),
worker(Pleroma.Stats, []) worker(Pleroma.Gopher.Server, [])
] ++ ] ++
if Mix.env() == :test, if Mix.env() == :test,
do: [], do: [],

42
lib/pleroma/config.ex Normal file
View file

@ -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

194
lib/pleroma/emoji.ex Normal file
View file

@ -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

View file

@ -36,6 +36,34 @@ def get_filters(%Pleroma.User{id: user_id} = user) do
Repo.all(query) Repo.all(query)
end 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 def create(%Pleroma.Filter{} = filter) do
Repo.insert(filter) Repo.insert(filter)
end end

View file

@ -2,6 +2,7 @@ defmodule Pleroma.Formatter do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.Emoji
@tag_regex ~r/\#\w+/u @tag_regex ~r/\#\w+/u
def parse_tags(text, data \\ %{}) do def parse_tags(text, data \\ %{}) do
@ -28,125 +29,10 @@ def parse_mentions(text) do
|> Enum.filter(fn {_match, user} -> user end) |> Enum.filter(fn {_match, user} -> user end)
end end
@finmoji [ def emojify(text) do
"a_trusted_friend", emojify(text, Emoji.get_all())
"alandislands", end
"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"
]
@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, nil), do: text
def emojify(text, emoji) do def emojify(text, emoji) do
@ -166,15 +52,11 @@ def emojify(text, emoji) do
end end
def get_emoji(text) when is_binary(text) do 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 end
def get_emoji(_), do: [] def get_emoji(_), do: []
def get_custom_emoji() do
@emoji
end
@link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui @link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui
@uri_schemes Application.get_env(:pleroma, :uri_schemes, []) @uri_schemes Application.get_env(:pleroma, :uri_schemes, [])

View file

@ -1,16 +1,16 @@
defmodule Pleroma.Gopher.Server do defmodule Pleroma.Gopher.Server do
use GenServer use GenServer
require Logger require Logger
@gopher Application.get_env(:pleroma, :gopher)
def start_link() do def start_link() do
ip = Keyword.get(@gopher, :ip, {0, 0, 0, 0}) config = Pleroma.Config.get(:gopher, [])
port = Keyword.get(@gopher, :port, 1234) ip = Keyword.get(config, :ip, {0, 0, 0, 0})
port = Keyword.get(config, :port, 1234)
GenServer.start_link(__MODULE__, [ip, port], []) GenServer.start_link(__MODULE__, [ip, port], [])
end end
def init([ip, port]) do 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}") Logger.info("Starting gopher server on #{port}")
:ranch.start_listener( :ranch.start_listener(
@ -37,9 +37,6 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.HTML alias Pleroma.HTML
@instance Application.get_env(:pleroma, :instance)
@gopher Application.get_env(:pleroma, :gopher)
def start_link(ref, socket, transport, opts) do def start_link(ref, socket, transport, opts) do
pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts]) pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts])
{:ok, pid} {:ok, pid}
@ -62,7 +59,7 @@ def info(text) do
def link(name, selector, type \\ 1) do def link(name, selector, type \\ 1) do
address = Pleroma.Web.Endpoint.host() 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" "#{type}#{name}\t#{selector}\t#{address}\t#{port}\r\n"
end end
@ -85,7 +82,7 @@ def render_activities(activities) do
end end
def response("") do 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("Public Timeline", "/main/public") <>
link("Federated Timeline", "/main/all") <> ".\r\n" link("Federated Timeline", "/main/all") <> ".\r\n"
end end

View file

@ -1,14 +1,12 @@
defmodule Pleroma.HTML do defmodule Pleroma.HTML do
alias HtmlSanitizeEx.Scrubber alias HtmlSanitizeEx.Scrubber
@markup Application.get_env(:pleroma, :markup)
defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber] defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber]
defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers
defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default] defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default]
def get_scrubbers() do def get_scrubbers() do
Keyword.get(@markup, :scrub_policy) Pleroma.Config.get([:markup, :scrub_policy])
|> get_scrubbers |> get_scrubbers
end 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_uri_attributes("a", ["href"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title"]) 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("b", [])
Meta.allow_tag_with_these_attributes("blockquote", []) Meta.allow_tag_with_these_attributes("blockquote", [])
Meta.allow_tag_with_these_attributes("br", []) Meta.allow_tag_with_these_attributes("br", [])

View file

@ -1,6 +1,6 @@
defmodule Pleroma.Notification do defmodule Pleroma.Notification do
use Ecto.Schema use Ecto.Schema
alias Pleroma.{User, Activity, Notification, Repo} alias Pleroma.{User, Activity, Notification, Repo, Object}
import Ecto.Query import Ecto.Query
schema "notifications" do schema "notifications" do
@ -42,6 +42,20 @@ def for_user(user, opts \\ %{}) do
Repo.all(query) Repo.all(query)
end 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 def get(%{id: user_id} = _user, id) do
query = query =
from( from(
@ -81,7 +95,7 @@ def dismiss(%{id: user_id} = _user, id) do
def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity) def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity)
when type in ["Create", "Like", "Announce", "Follow"] do 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) notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications} {:ok, notifications}
@ -99,4 +113,64 @@ def create_notification(%Activity{} = activity, %User{} = user) do
notification notification
end end
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 end

View file

@ -1,6 +1,6 @@
defmodule Pleroma.Object do defmodule Pleroma.Object do
use Ecto.Schema use Ecto.Schema
alias Pleroma.{Repo, Object} alias Pleroma.{Repo, Object, Activity}
import Ecto.{Query, Changeset} import Ecto.{Query, Changeset}
schema "objects" do schema "objects" do
@ -37,7 +37,7 @@ def get_cached_by_ap_id(ap_id) do
else else
key = "object:#{ap_id}" key = "object:#{ap_id}"
Cachex.fetch!(:user_cache, key, fn _ -> Cachex.fetch!(:object_cache, key, fn _ ->
object = get_by_ap_id(ap_id) object = get_by_ap_id(ap_id)
if object do if object do
@ -52,4 +52,12 @@ def get_cached_by_ap_id(ap_id) do
def context_mapping(context) do def context_mapping(context) do
Object.change(%Object{}, %{data: %{"id" => context}}) Object.change(%Object{}, %{data: %{"id" => context}})
end 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 end

View file

@ -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

View file

@ -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

View file

@ -1,64 +1,74 @@
defmodule Pleroma.Upload do defmodule Pleroma.Upload do
alias Ecto.UUID alias Ecto.UUID
@storage_backend Application.get_env(:pleroma, Pleroma.Upload) def check_file_size(path, nil), do: true
|> Keyword.fetch!(:uploader)
def store(%Plug.Upload{} = file, should_dedupe) do def check_file_size(path, size_limit) do
content_type = get_content_type(file.path) {:ok, %{size: size}} = File.stat(path)
size <= size_limit
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
}
end 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/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data) parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace) 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) {:ok, url_path} = uploader().put_file(name, uuid, tmp_path, content_type, should_dedupe)
strip_exif_data(content_type, tmp_path)
name = %{
create_name( "type" => "Image",
String.downcase(Base.encode16(:crypto.hash(:sha256, data))), "url" => [
parsed["filetype"], %{
content_type "type" => "Link",
) "mediaType" => content_type,
"href" => url_path
{:ok, url_path} = @storage_backend.put_file(name, uuid, tmp_path, content_type, should_dedupe) }
],
%{ "name" => name
"type" => "Image", }
"url" => [ else
%{ _e -> nil
"type" => "Link", end
"mediaType" => content_type,
"href" => url_path
}
],
"name" => name
}
end end
@doc """ @doc """
@ -167,4 +177,8 @@ def get_content_type(file) do
_e -> "application/octet-stream" _e -> "application/octet-stream"
end end
end end
defp uploader() do
Pleroma.Config.get!([Pleroma.Upload, :uploader])
end
end end

View file

@ -1,10 +1,13 @@
defmodule Pleroma.Uploaders.S3 do defmodule Pleroma.Uploaders.S3 do
alias Pleroma.Web.MediaProxy
@behaviour Pleroma.Uploaders.Uploader @behaviour Pleroma.Uploaders.Uploader
def put_file(name, uuid, path, content_type, _should_dedupe) do def put_file(name, uuid, path, content_type, _should_dedupe) do
settings = Application.get_env(:pleroma, Pleroma.Uploaders.S3) settings = Application.get_env(:pleroma, Pleroma.Uploaders.S3)
bucket = Keyword.fetch!(settings, :bucket) bucket = Keyword.fetch!(settings, :bucket)
public_endpoint = Keyword.fetch!(settings, :public_endpoint) public_endpoint = Keyword.fetch!(settings, :public_endpoint)
force_media_proxy = Keyword.fetch!(settings, :force_media_proxy)
{:ok, file_data} = File.read(path) {:ok, file_data} = File.read(path)
@ -19,7 +22,16 @@ def put_file(name, uuid, path, content_type, _should_dedupe) do
]) ])
|> ExAws.request() |> 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 end
defp encode(name) do defp encode(name) do

View file

@ -1,11 +1,9 @@
defmodule Pleroma.Uploaders.Swift.Keystone do defmodule Pleroma.Uploaders.Swift.Keystone do
use HTTPoison.Base use HTTPoison.Base
@settings Application.get_env(:pleroma, Pleroma.Uploaders.Swift)
def process_url(url) do def process_url(url) do
Enum.join( Enum.join(
[Keyword.fetch!(@settings, :auth_url), url], [Pleroma.Config.get!([Pleroma.Uploaders.Swift, :auth_url]), url],
"/" "/"
) )
end end
@ -16,9 +14,10 @@ def process_response_body(body) do
end end
def get_token() do def get_token() do
username = Keyword.fetch!(@settings, :username) settings = Pleroma.Config.get(Pleroma.Uploaders.Swift)
password = Keyword.fetch!(@settings, :password) username = Keyword.fetch!(settings, :username)
tenant_id = Keyword.fetch!(@settings, :tenant_id) password = Keyword.fetch!(settings, :password)
tenant_id = Keyword.fetch!(settings, :tenant_id)
case post( case post(
"/tokens", "/tokens",

View file

@ -1,17 +1,15 @@
defmodule Pleroma.Uploaders.Swift.Client do defmodule Pleroma.Uploaders.Swift.Client do
use HTTPoison.Base use HTTPoison.Base
@settings Application.get_env(:pleroma, Pleroma.Uploaders.Swift)
def process_url(url) do def process_url(url) do
Enum.join( Enum.join(
[Keyword.fetch!(@settings, :storage_url), url], [Pleroma.Config.get!([Pleroma.Uploaders.Swift, :storage_url]), url],
"/" "/"
) )
end end
def upload_file(filename, body, content_type) do 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() token = Pleroma.Uploaders.Swift.Keystone.get_token()
case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do

View file

@ -295,6 +295,7 @@ def update_and_set_cache(changeset) do
def invalidate_cache(user) do def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}") Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}") Cachex.del(:user_cache, "nickname:#{user.nickname}")
Cachex.del(:user_cache, "user_info:#{user.id}")
end end
def get_cached_by_ap_id(ap_id) do 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) update_and_set_cache(cs)
end end
def get_notified_from_activity_query(to) do def get_users_from_set_query(ap_ids, false) do
from( from(
u in User, 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 where: u.local == true
) )
end end
def get_notified_from_activity(%Activity{recipients: to, data: %{"type" => "Announce"} = data}) do def get_users_from_set(ap_ids, local_only \\ true) do
object = Object.normalize(data["object"]) get_users_from_set_query(ap_ids, local_only)
actor = User.get_cached_by_ap_id(data["actor"]) |> Repo.all()
# 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)
end end
def get_recipients_from_activity(%Activity{recipients: to}) do def get_recipients_from_activity(%Activity{recipients: to}) do
@ -622,8 +612,8 @@ def moderator_user_query() do
) )
end end
def deactivate(%User{} = user) do def deactivate(%User{} = user, status \\ true) do
new_info = Map.put(user.info, "deactivated", true) new_info = Map.put(user.info, "deactivated", status)
cs = User.info_changeset(user, %{info: new_info}) cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs) update_and_set_cache(cs)
end end
@ -656,7 +646,7 @@ def delete(%User{} = user) do
end end
end) end)
:ok {:ok, user}
end end
def html_filter_policy(%User{info: %{"no_rich_text" => true}}) do def html_filter_policy(%User{info: %{"no_rich_text" => true}}) do

View file

@ -10,8 +10,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
@httpoison Application.get_env(:pleroma, :httpoison) @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 # For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary. # that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do defp get_recipients(%{"type" => "Announce"} = data) do
@ -44,7 +42,7 @@ defp get_recipients(data) do
defp check_actor_is_active(actor) do defp check_actor_is_active(actor) do
if not is_nil(actor) do if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor), with user <- User.get_cached_by_ap_id(actor),
nil <- user.info["deactivated"] do false <- !!user.info["deactivated"] do
:ok :ok
else else
_e -> :reject _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"] "to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"]
} }
with Repo.delete(object), with {:ok, _} <- Object.delete(object),
Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity), :ok <- maybe_federate(activity),
{:ok, _actor} <- User.decrease_note_count(user) do {:ok, _actor} <- User.decrease_note_count(user) do
@ -575,9 +572,12 @@ def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
|> Enum.reverse() |> Enum.reverse()
end end
def upload(file) do def upload(file, size_limit \\ nil) do
data = Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media]) with data <-
Repo.insert(%Object{data: 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 end
def user_data_from_user_object(data) do def user_data_from_user_object(data) do
@ -657,14 +657,12 @@ def make_user_from_nickname(nickname) do
end end
end end
@quarantined_instances Keyword.get(@instance, :quarantined_instances, [])
def should_federate?(inbox, public) do def should_federate?(inbox, public) do
if public do if public do
true true
else else
inbox_info = URI.parse(inbox) 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
end end
@ -793,9 +791,10 @@ def entire_thread_visible_for_user?(nil, user), do: false
# child # child
def entire_thread_visible_for_user?( def entire_thread_visible_for_user?(
%Activity{data: %{"object" => %{"inReplyTo" => _parent_id}}} = tail, %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
user user
) do )
when is_binary(parent_id) do
parent = Activity.get_in_reply_to_activity(tail) parent = Activity.get_in_reply_to_activity(tail)
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user) visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
end end

View file

@ -11,6 +11,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
action_fallback(:errors) 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 def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do

View file

@ -3,10 +3,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
@behaviour Pleroma.Web.ActivityPub.MRF @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 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"] child = object["object"]

View file

@ -2,10 +2,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
alias Pleroma.User alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF @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 @impl true
def filter(%{"type" => "Create"} = object) do def filter(%{"type" => "Create"} = object) do
user = User.get_cached_by_ap_id(object["actor"]) user = User.get_cached_by_ap_id(object["actor"])
@ -20,6 +16,8 @@ def filter(%{"type" => "Create"} = object) do
true -> "direct" true -> "direct"
end end
policy = Pleroma.Config.get(:mrf_rejectnonpublic)
case visibility do case visibility do
"public" -> "public" ->
{:ok, object} {:ok, object}
@ -28,14 +26,14 @@ def filter(%{"type" => "Create"} = object) do
{:ok, object} {:ok, object}
"followers" -> "followers" ->
with true <- @allow_followersonly do with true <- Keyword.get(policy, :allow_followersonly) do
{:ok, object} {:ok, object}
else else
_e -> {:reject, nil} _e -> {:reject, nil}
end end
"direct" -> "direct" ->
with true <- @allow_direct do with true <- Keyword.get(policy, :allow_direct) do
{:ok, object} {:ok, object}
else else
_e -> {:reject, nil} _e -> {:reject, nil}

View file

@ -2,60 +2,76 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
alias Pleroma.User alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF @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) cond do
defp check_accept(%{host: actor_host} = actor_info, object) accepts == [] -> {:ok, object}
when length(@accept) > 0 and not (actor_host in @accept) do actor_host == Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
{:reject, nil} Enum.member?(accepts, actor_host) -> {:ok, object}
true -> {:reject, nil}
end
end end
defp check_accept(actor_info, object), do: {:ok, object} defp check_reject(%{host: actor_host} = _actor_info, object) do
if Enum.member?(Pleroma.Config.get([:mrf_simple, :reject]), actor_host) do
@reject Keyword.get(@mrf_policy, :reject) {:reject, nil}
defp check_reject(%{host: actor_host} = actor_info, object) when actor_host in @reject do else
{:reject, nil} {:ok, object}
end
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} {:ok, object}
end 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( defp check_media_nsfw(
%{host: actor_host} = actor_info, %{host: actor_host} = _actor_info,
%{ %{
"type" => "Create", "type" => "Create",
"object" => %{"attachment" => child_attachment} = child_object "object" => %{"attachment" => child_attachment} = child_object
} = object } = object
) )
when actor_host in @media_nsfw and length(child_attachment) > 0 do when length(child_attachment) > 0 do
tags = (child_object["tag"] || []) ++ ["nsfw"] object =
child_object = Map.put(child_object, "tags", tags) if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_nsfw]), actor_host) do
child_object = Map.put(child_object, "sensitive", true) tags = (child_object["tag"] || []) ++ ["nsfw"]
object = Map.put(object, "object", child_object) 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} {:ok, object}
end 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) do
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
object = object =
if "https://www.w3.org/ns/activitystreams#Public" in object["to"] and with true <-
user.follower_address in object["cc"] do 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 = to =
List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++ List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++
[user.follower_address] [user.follower_address]
@ -68,14 +84,12 @@ defp check_ftl_removal(%{host: actor_host} = actor_info, object)
|> Map.put("to", to) |> Map.put("to", to)
|> Map.put("cc", cc) |> Map.put("cc", cc)
else else
object _ -> object
end end
{:ok, object} {:ok, object}
end end
defp check_ftl_removal(actor_info, object), do: {:ok, object}
@impl true @impl true
def filter(object) do def filter(object) do
actor_info = URI.parse(object["actor"]) actor_info = URI.parse(object["actor"])

View file

@ -57,6 +57,7 @@ def fix_object(object) do
object object
|> fix_actor |> fix_actor
|> fix_attachments |> fix_attachments
|> fix_url
|> fix_context |> fix_context
|> fix_in_reply_to |> fix_in_reply_to
|> fix_emoji |> fix_emoji
@ -171,6 +172,27 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachme
def fix_attachments(object), do: object 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 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end) 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 # - tags
# - emoji # - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
when objtype in ["Article", "Note", "Video"] do when objtype in ["Article", "Note", "Video", "Page"] do
actor = get_actor(data) actor = get_actor(data)
data = data =
@ -484,9 +506,6 @@ def handle_incoming(
end end
end end
@ap_config Application.get_env(:pleroma, :activitypub)
@accept_blocks Keyword.get(@ap_config, :accept_blocks)
def handle_incoming( def handle_incoming(
%{ %{
"type" => "Undo", "type" => "Undo",
@ -495,7 +514,7 @@ def handle_incoming(
"id" => id "id" => id
} = _data } = _data
) do ) 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{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
%User{} = blocker <- User.get_or_fetch_by_ap_id(blocker), %User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
@ -509,7 +528,7 @@ def handle_incoming(
def handle_incoming( def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
) do ) 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{local: true} = blocked = User.get_cached_by_ap_id(blocked),
%User{} = blocker = User.get_or_fetch_by_ap_id(blocker), %User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
@ -570,6 +589,8 @@ def prepare_object(object) do
|> prepare_attachments |> prepare_attachments
|> set_conversation |> set_conversation
|> set_reply_to_uri |> set_reply_to_uri
|> strip_internal_fields
|> strip_internal_tags
end end
# @doc # @doc
@ -585,7 +606,7 @@ def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = obj
data = data =
data data
|> Map.put("object", object) |> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams") |> Map.merge(Utils.make_json_ld_header())
{:ok, data} {:ok, data}
end end
@ -604,7 +625,7 @@ def prepare_outgoing(%{"type" => "Accept"} = data) do
data = data =
data data
|> Map.put("object", object) |> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams") |> Map.merge(Utils.make_json_ld_header())
{:ok, data} {:ok, data}
end end
@ -622,7 +643,7 @@ def prepare_outgoing(%{"type" => "Reject"} = data) do
data = data =
data data
|> Map.put("object", object) |> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams") |> Map.merge(Utils.make_json_ld_header())
{:ok, data} {:ok, data}
end end
@ -632,7 +653,7 @@ def prepare_outgoing(%{"type" => _type} = data) do
data = data =
data data
|> maybe_fix_object_url |> maybe_fix_object_url
|> Map.put("@context", "https://www.w3.org/ns/activitystreams") |> Map.merge(Utils.make_json_ld_header())
{:ok, data} {:ok, data}
end end
@ -674,12 +695,9 @@ def add_hashtags(object) do
end end
def add_mention_tags(object) do def add_mention_tags(object) do
recipients = object["to"] ++ (object["cc"] || [])
mentions = mentions =
recipients object
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end) |> Utils.get_notified_from_object()
|> Enum.filter(& &1)
|> Enum.map(fn user -> |> Enum.map(fn user ->
%{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"} %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
end) end)
@ -739,6 +757,29 @@ def prepare_attachments(object) do
|> Map.put("attachment", attachments) |> Map.put("attachment", attachments)
end 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 defp user_upgrade_task(user) do
old_follower_address = User.ap_followers(user) old_follower_address = User.ap_followers(user)

View file

@ -1,11 +1,13 @@
defmodule Pleroma.Web.ActivityPub.Utils do 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.Router.Helpers
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Ecto.{Changeset, UUID} alias Ecto.{Changeset, UUID}
import Ecto.Query import Ecto.Query
require Logger 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, # Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have. # so figure out what the actor's URI is based on what we have.
def get_ap_id(object) do def get_ap_id(object) do
@ -70,18 +72,7 @@ def make_json_ld_header do
%{ %{
"@context" => [ "@context" => [
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1", "#{Web.base_url()}/schemas/litepub-0.1.jsonld"
%{
"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"
}
] ]
} }
end end
@ -106,6 +97,21 @@ def generate_id(type) do
"#{Web.base_url()}/#{type}/#{UUID.generate()}" "#{Web.base_url()}/#{type}/#{UUID.generate()}"
end 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 def create_context(context) do
context = context || generate_id("contexts") context = context || generate_id("contexts")
changeset = Object.context_mapping(context) 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. Inserts a full object if it is contained in an activity.
""" """
def insert_full_object(%{"object" => %{"type" => type} = object_data}) 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 with {:ok, _} <- Object.create(object_data) do
:ok :ok
end end

View file

@ -1,27 +1,23 @@
defmodule Pleroma.Web.ActivityPub.ObjectView do defmodule Pleroma.Web.ActivityPub.ObjectView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.{Object, Activity}
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
def render("object.json", %{object: object}) do def render("object.json", %{object: %Object{} = object}) do
base = %{ base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
"@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"
}
]
}
additional = Transmogrifier.prepare_object(object.data) additional = Transmogrifier.prepare_object(object.data)
Map.merge(base, additional) Map.merge(base, additional)
end 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 end

View file

@ -17,7 +17,6 @@ def render("user.json", %{user: %{nickname: nil} = user}) do
public_key = :public_key.pem_encode([public_key]) public_key = :public_key.pem_encode([public_key])
%{ %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => user.ap_id, "id" => user.ap_id,
"type" => "Application", "type" => "Application",
"following" => "#{user.ap_id}/following", "following" => "#{user.ap_id}/following",
@ -36,6 +35,7 @@ def render("user.json", %{user: %{nickname: nil} = user}) do
"sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox" "sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
} }
} }
|> Map.merge(Utils.make_json_ld_header())
end end
def render("user.json", %{user: user}) do def render("user.json", %{user: user}) do

View file

@ -36,7 +36,6 @@ def unrepeat(id_or_ap_id, user) do
def favorite(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), 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 object <- Object.normalize(activity.data["object"]["id"]) do
ActivityPub.like(user, object) ActivityPub.like(user, object)
else else
@ -47,7 +46,6 @@ def favorite(id_or_ap_id, user) do
def unfavorite(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), 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 object <- Object.normalize(activity.data["object"]["id"]) do
ActivityPub.unlike(user, object) ActivityPub.unlike(user, object)
else 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" def get_visibility(_), do: "public"
@instance Application.get_env(:pleroma, :instance) defp get_content_type(content_type) do
@allowed_post_formats Keyword.get(@instance, :allowed_post_formats) 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 def post(user, %{"status" => status} = data) do
visibility = get_visibility(data) visibility = get_visibility(data)
limit = Pleroma.Config.get([:instance, :limit])
with status <- String.trim(status), with status <- String.trim(status),
attachments <- attachments_from_ids(data["media_ids"]), attachments <- attachments_from_ids(data["media_ids"]),
@ -100,7 +100,7 @@ def post(user, %{"status" => status} = data) do
context <- make_context(inReplyTo), context <- make_context(inReplyTo),
cw <- data["spoiler_text"], cw <- data["spoiler_text"],
full_payload <- String.trim(status <> (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 <- object <-
make_note_data( make_note_data(
user.ap_id, user.ap_id,

View file

@ -2,6 +2,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.{Repo, Object, Formatter, Activity} alias Pleroma.{Repo, Object, Formatter, Activity}
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy
alias Pleroma.User alias Pleroma.User
alias Calendar.Strftime alias Calendar.Strftime
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
@ -18,6 +19,8 @@ def get_by_id_or_ap_id(id) do
end end
end end
def get_replied_to_activity(""), do: nil
def get_replied_to_activity(id) when not is_nil(id) do def get_replied_to_activity(id) when not is_nil(id) do
Repo.get(Activity, id) Repo.get(Activity, id)
end end
@ -31,21 +34,29 @@ def attachments_from_ids(ids) do
end end
def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do 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) 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 if inReplyTo do
{to, Enum.uniq([inReplyTo.data["actor"] | cc])} {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
else else
{to, cc} {to, cc}
end end
end end
def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
{to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "public") mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
{cc, to}
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 end
def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do 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 def add_attachments(text, attachments) do
attachment_text = attachment_text =
Enum.map(attachments, fn Enum.map(attachments, fn
%{"url" => [%{"href" => href} | _]} -> %{"url" => [%{"href" => href} | _]} = attachment ->
name = URI.decode(Path.basename(href)) name = attachment["name"] || URI.decode(Path.basename(href))
href = MediaProxy.url(href)
"<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>" "<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"
_ -> _ ->

View file

@ -11,13 +11,17 @@ defmodule Pleroma.Web.Endpoint do
# #
# You should set gzip to true if you are running phoenix.digest # You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production. # 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: "/media", from: Pleroma.Uploaders.Local.upload_path(), gzip: false)
plug( plug(
Plug.Static, Plug.Static,
at: "/", at: "/",
from: :pleroma, 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 # Code reloading can be explicitly enabled under the
@ -42,13 +46,18 @@ defmodule Pleroma.Web.Endpoint do
plug(Plug.MethodOverride) plug(Plug.MethodOverride)
plug(Plug.Head) 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, # The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with. # this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it. # Set :encryption_salt if you would also like to encrypt it.
plug( plug(
Plug.Session, Plug.Session,
store: :cookie, store: :cookie,
key: "_pleroma_key", key: cookie_name,
signing_salt: "CqaoopA2", signing_salt: "CqaoopA2",
http_only: true, http_only: true,
secure: secure:

View file

@ -7,13 +7,12 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.OStatus
require Logger require Logger
@websub Application.get_env(:pleroma, :websub) @websub Application.get_env(:pleroma, :websub)
@ostatus Application.get_env(:pleroma, :ostatus) @ostatus Application.get_env(:pleroma, :ostatus)
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
@instance Application.get_env(:pleroma, :instance)
@federating Keyword.get(@instance, :federating)
@max_jobs 20 @max_jobs 20
def init(args) do def init(args) do
@ -65,15 +64,17 @@ def handle(:publish, activity) do
{:ok, actor} = WebFinger.ensure_keys_present(actor) {:ok, actor} = WebFinger.ensure_keys_present(actor)
if ActivityPub.is_public?(activity) do if ActivityPub.is_public?(activity) do
Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end) if OStatus.is_representable?(activity) do
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) 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) Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end)
Pleroma.Web.Salmon.publish(actor, activity) 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) Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Pleroma.Web.ActivityPub.Relay.publish(activity) Relay.publish(activity)
end end
end end
@ -147,7 +148,7 @@ def handle(type, _) do
end end
def enqueue(type, payload, priority \\ 1) do def enqueue(type, payload, priority \\ 1) do
if @federating do if Pleroma.Config.get([:instance, :federating]) do
if Mix.env() == :test do if Mix.env() == :test do
handle(type, payload) handle(type, payload)
else else

View file

@ -35,6 +35,14 @@ def create_app(conn, params) do
def update_credentials(%{assigns: %{user: user}} = conn, params) do def update_credentials(%{assigns: %{user: user}} = conn, params) do
original_user = user 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 = params =
if bio = params["note"] do if bio = params["note"] do
Map.put(params, "bio", bio) Map.put(params, "bio", bio)
@ -52,7 +60,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
user = user =
if avatar = params["avatar"] do if avatar = params["avatar"] do
with %Plug.Upload{} <- avatar, 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}), change = Ecto.Changeset.change(user, %{avatar: object.data}),
{:ok, user} = User.update_and_set_cache(change) do {:ok, user} = User.update_and_set_cache(change) do
user user
@ -66,7 +74,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
user = user =
if banner = params["header"] do if banner = params["header"] do
with %Plug.Upload{} <- banner, 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), new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}), change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do {: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
end end
@instance Application.get_env(:pleroma, :instance)
@mastodon_api_level "2.5.0" @mastodon_api_level "2.5.0"
def masto_instance(conn, _params) do def masto_instance(conn, _params) do
instance = Pleroma.Config.get(:instance)
response = %{ response = %{
uri: Web.base_url(), uri: Web.base_url(),
title: Keyword.get(@instance, :name), title: Keyword.get(instance, :name),
description: Keyword.get(@instance, :description), description: Keyword.get(instance, :description),
version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})", version: "#{@mastodon_api_level} (compatible; #{Keyword.get(instance, :version)})",
email: Keyword.get(@instance, :email), email: Keyword.get(instance, :email),
urls: %{ urls: %{
streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws") streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
}, },
stats: Stats.get_stats(), stats: Stats.get_stats(),
thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg", thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
max_toot_chars: Keyword.get(@instance, :limit) max_toot_chars: Keyword.get(instance, :limit)
} }
json(conn, response) json(conn, response)
@ -150,7 +159,7 @@ def peers(conn, _params) do
end end
defp mastodonized_emoji do defp mastodonized_emoji do
Pleroma.Formatter.get_custom_emoji() Pleroma.Emoji.get_all()
|> Enum.map(fn {shortcode, relative_url} -> |> Enum.map(fn {shortcode, relative_url} ->
url = to_string(URI.merge(Web.base_url(), 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
end end
def dm_timeline(%{assigns: %{user: user}} = conn, _params) do def dm_timeline(%{assigns: %{user: user}} = conn, params) do
query = 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) 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}) render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
end 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 def update_media(%{assigns: %{user: _}} = conn, data) do
with %Object{} = object <- Repo.get(Object, data["id"]), with %Object{} = object <- Repo.get(Object, data["id"]),
true <- is_binary(data["description"]), true <- is_binary(data["description"]),
@ -500,6 +518,7 @@ def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
|> Map.put("type", "Create") |> Map.put("type", "Create")
|> Map.put("local_only", local_only) |> Map.put("local_only", local_only)
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("tag", String.downcase(params["tag"]))
activities = activities =
ActivityPub.fetch_public_activities(params) ActivityPub.fetch_public_activities(params)
@ -572,15 +591,16 @@ def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) d
end end
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 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id), with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.maybe_direct_follow(follower, followed), {:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed), {:ok, _activity} <- ActivityPub.follow(follower, followed),
{:ok, 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}) render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
else else
{:error, message} -> {:error, message} ->
@ -871,6 +891,8 @@ def index(%{assigns: %{user: user}} = conn, _params) do
if user && token do if user && token do
mastodon_emoji = mastodonized_emoji() mastodon_emoji = mastodonized_emoji()
limit = Pleroma.Config.get([:instance, :limit])
accounts = accounts =
Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) 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, auto_play_gif: false,
display_sensitive_media: false, display_sensitive_media: false,
reduce_motion: false, reduce_motion: false,
max_toot_chars: Keyword.get(@instance, :limit) max_toot_chars: limit
}, },
rights: %{ rights: %{
delete_others_notice: !!user.info["is_moderator"] delete_others_notice: !!user.info["is_moderator"]
@ -950,7 +972,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do
push_subscription: nil, push_subscription: nil,
accounts: accounts, accounts: accounts,
custom_emojis: mastodon_emoji, custom_emojis: mastodon_emoji,
char_limit: Keyword.get(@instance, :limit) char_limit: limit
} }
|> Jason.encode!() |> Jason.encode!()
@ -976,9 +998,29 @@ def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _para
end end
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 def login(conn, _) do
conn with {:ok, app} <- get_or_make_app() do
|> render(MastodonView, "login.html", %{error: false}) 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 end
defp get_or_make_app() do defp get_or_make_app() do
@ -997,22 +1039,6 @@ defp get_or_make_app() do
end end
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 def logout(conn, _) do
conn conn
|> clear_session |> clear_session
@ -1156,18 +1182,15 @@ def errors(conn, _) do
|> json("Something went wrong") |> json("Something went wrong")
end end
@suggestions Application.get_env(:pleroma, :suggestions)
def suggestions(%{assigns: %{user: user}} = conn, _) do def suggestions(%{assigns: %{user: user}} = conn, _) do
if Keyword.get(@suggestions, :enabled, false) do suggestions = Pleroma.Config.get(:suggestions)
api = Keyword.get(@suggestions, :third_party_engine, "")
timeout = Keyword.get(@suggestions, :timeout, 5000)
limit = Keyword.get(@suggestions, :limit, 23)
host = if Keyword.get(suggestions, :enabled, false) do
Application.get_env(:pleroma, Pleroma.Web.Endpoint) api = Keyword.get(suggestions, :third_party_engine, "")
|> Keyword.get(:url) timeout = Keyword.get(suggestions, :timeout, 5000)
|> Keyword.get(:host) limit = Keyword.get(suggestions, :limit, 23)
host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
user = user.nickname user = user.nickname
url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user) url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)

View file

@ -26,15 +26,19 @@ def connect(params, socket) do
"list", "list",
"hashtag" "hashtag"
] <- params["stream"] do ] <- params["stream"] do
topic = if stream == "list", do: "list:#{params["list"]}", else: stream topic =
socket_stream = if stream == "hashtag", do: "hashtag:#{params["tag"]}", else: stream case stream do
"hashtag" -> "hashtag:#{params["tag"]}"
"list" -> "list:#{params["list"]}"
_ -> stream
end
socket = socket =
socket socket
|> assign(:topic, topic) |> assign(:topic, topic)
|> assign(:user, user) |> assign(:user, user)
Pleroma.Web.Streamer.add_socket(socket_stream, socket) Pleroma.Web.Streamer.add_socket(topic, socket)
{:ok, socket} {:ok, socket}
else else
_e -> :error _e -> :error

View file

@ -61,7 +61,7 @@ def render(
in_reply_to_id: nil, in_reply_to_id: nil,
in_reply_to_account_id: nil, in_reply_to_account_id: nil,
reblog: reblogged, reblog: reblogged,
content: reblogged[:content], content: reblogged[:content] || "",
created_at: created_at, created_at: created_at,
reblogs_count: 0, reblogs_count: 0,
replies_count: 0, replies_count: 0,
@ -166,7 +166,7 @@ def render("status.json", _) do
def render("attachment.json", %{attachment: attachment}) do def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"] [attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image" media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
href = attachment_url["href"] href = attachment_url["href"] |> MediaProxy.url()
type = type =
cond do cond do
@ -180,9 +180,9 @@ def render("attachment.json", %{attachment: attachment}) do
%{ %{
id: to_string(attachment["id"] || hash_id), id: to_string(attachment["id"] || hash_id),
url: MediaProxy.url(href), url: href,
remote_url: href, remote_url: href,
preview_url: MediaProxy.url(href), preview_url: href,
text_url: href, text_url: href,
type: type, type: type,
description: attachment["name"] description: attachment["name"]
@ -230,24 +230,24 @@ def render_content(%{"type" => "Video"} = object) do
if !!name and name != "" do if !!name and name != "" do
"<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}" "<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
else else
object["content"] object["content"] || ""
end end
content content
end 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"] summary = object["name"]
content = content =
if !!summary and summary != "" do if !!summary and summary != "" and is_bitstring(object["url"]) do
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}" "<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
else else
object["content"] object["content"] || ""
end end
content content
end end
def render_content(object), do: object["content"] def render_content(object), do: object["content"] || ""
end end

View file

@ -11,15 +11,47 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
error: "public, must-revalidate, max-age=160" 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, []) config = Application.get_env(:pleroma, :media_proxy, [])
with true <- Keyword.get(config, :enabled, false), with true <- Keyword.get(config, :enabled, false),
{:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url), {: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 conn
|> put_resp_content_type(content_type) |> put_resp_content_type(content_type)
|> set_cache_header(:default) |> 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) |> send_resp(200, body)
else else
false -> 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. # 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. # 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 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
end end

View file

@ -15,7 +15,10 @@ def url(url) do
base64 = Base.url_encode64(url, @base64_opts) base64 = Base.url_encode64(url, @base64_opts)
sig = :crypto.hmac(:sha, secret, base64) sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts) 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
end end

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.{User, Repo} alias Pleroma.{User, Repo}
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
plug(Pleroma.Web.FederatingPlug)
def schemas(conn, _params) do def schemas(conn, _params) do
response = %{ response = %{
links: [ links: [
@ -113,6 +115,12 @@ def nodeinfo(conn, %{"version" => "2.0"}) do
staffAccounts: staff_accounts, staffAccounts: staff_accounts,
federation: federation_response, federation: federation_response,
postFormats: Keyword.get(instance, :allowed_post_formats), 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 features: features
} }
} }

View file

@ -33,25 +33,35 @@ def create_authorization(conn, %{
true <- Pbkdf2.checkpw(password, user.password_hash), true <- Pbkdf2.checkpw(password, user.password_hash),
%App{} = app <- Repo.get_by(App, client_id: client_id), %App{} = app <- Repo.get_by(App, client_id: client_id),
{:ok, auth} <- Authorization.create_authorization(app, user) do {:ok, auth} <- Authorization.create_authorization(app, user) do
if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do # Special case: Local MastodonFE.
render(conn, "results.html", %{ redirect_uri =
auth: auth if redirect_uri == "." do
}) mastodon_api_url(conn, :login)
else else
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" redirect_uri
url = "#{redirect_uri}#{connector}" end
url_params = %{:code => auth.token}
url_params = cond do
if params["state"] do redirect_uri == "urn:ietf:wg:oauth:2.0:oob" ->
Map.put(url_params, :state, params["state"]) render(conn, "results.html", %{
else auth: auth
url_params })
end
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 end
end end
@ -133,8 +143,11 @@ def token_revoke(conn, %{"token" => token} = params) do
end end
end end
# XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be
# decoding it. Investigate sometime.
defp fix_padding(token) do defp fix_padding(token) do
token token
|> URI.decode()
|> Base.url_decode64!(padding: false) |> Base.url_decode64!(padding: false)
|> Base.url_encode64() |> Base.url_encode64()
end end

View file

@ -11,6 +11,21 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler} alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler}
alias Pleroma.Web.ActivityPub.Transmogrifier 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 def feed_path(user) do
"#{user.ap_id}/feed.atom" "#{user.ap_id}/feed.atom"
end end

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
action_fallback(:errors) action_fallback(:errors)
def feed_redirect(conn, %{"nickname" => nickname}) do def feed_redirect(conn, %{"nickname" => nickname}) do

View file

@ -3,12 +3,6 @@ defmodule Pleroma.Web.Router do
alias Pleroma.{Repo, User, Web.Router} 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 pipeline :api do
plug(:accepts, ["json"]) plug(:accepts, ["json"])
plug(:fetch_session) plug(:fetch_session)
@ -243,11 +237,7 @@ defmodule Pleroma.Web.Router do
end end
scope "/api", Pleroma.Web do scope "/api", Pleroma.Web do
if @public do pipe_through(:api)
pipe_through(:api)
else
pipe_through(:authenticated_api)
end
get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline) 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/friends_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline) get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
get("/statuses/mentions_timeline", 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) 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/update", TwitterAPI.Controller, :status_update)
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet) post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet) 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/feed", OStatus.OStatusController, :feed)
get("/users/:nickname", OStatus.OStatusController, :feed_redirect) get("/users/:nickname", OStatus.OStatusController, :feed_redirect)
if @federating do post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming) post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request) get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation) post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
end
end end
pipeline :activitypub do pipeline :activitypub do
@ -353,31 +346,27 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/outbox", ActivityPubController, :outbox) get("/users/:nickname/outbox", ActivityPubController, :outbox)
end end
if @federating do scope "/relay", Pleroma.Web.ActivityPub do
if @allow_relay do pipe_through(:ap_relay)
scope "/relay", Pleroma.Web.ActivityPub do get("/", ActivityPubController, :relay)
pipe_through(:ap_relay) end
get("/", ActivityPubController, :relay)
end
end
scope "/", Pleroma.Web.ActivityPub do scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub) pipe_through(:activitypub)
post("/users/:nickname/inbox", ActivityPubController, :inbox) post("/users/:nickname/inbox", ActivityPubController, :inbox)
post("/inbox", ActivityPubController, :inbox) post("/inbox", ActivityPubController, :inbox)
end end
scope "/.well-known", Pleroma.Web do scope "/.well-known", Pleroma.Web do
pipe_through(:well_known) pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta) get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger) get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas) get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end end
scope "/nodeinfo", Pleroma.Web do scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo) get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
end end
scope "/", Pleroma.Web.MastodonAPI do scope "/", Pleroma.Web.MastodonAPI do
@ -390,12 +379,12 @@ defmodule Pleroma.Web.Router do
end end
pipeline :remote_media do pipeline :remote_media do
plug(:accepts, ["html"])
end end
scope "/proxy/", Pleroma.Web.MediaProxy do scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through(:remote_media) pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote) get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end end
scope "/", Fallback do scope "/", Fallback do

View file

@ -1,11 +0,0 @@
<h2>Login to Mastodon Frontend</h2>
<%= if @error do %>
<h2><%= @error %></h2>
<% end %>
<%= form_for @conn, mastodon_api_path(@conn, :login), [as: "authorization"], fn f -> %>
<%= text_input f, :name, placeholder: "Username or email" %>
<br>
<%= password_input f, :password, placeholder: "Password" %>
<br>
<%= submit "Log in" %>
<% end %>

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
alias Pleroma.Formatter alias Pleroma.{Formatter, Emoji}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.{Repo, PasswordResetToken, User} alias Pleroma.{Repo, PasswordResetToken, User}
@ -134,19 +134,20 @@ def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}
end end
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 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 case get_format(conn) do
"xml" -> "xml" ->
response = """ response = """
<config> <config>
<site> <site>
<name>#{Keyword.get(@instance, :name)}</name> <name>#{Keyword.get(instance, :name)}</name>
<site>#{Web.base_url()}</site> <site>#{Web.base_url()}</site>
<textlimit>#{Keyword.get(@instance, :limit)}</textlimit> <textlimit>#{Keyword.get(instance, :limit)}</textlimit>
<closed>#{!Keyword.get(@instance, :registrations_open)}</closed> <closed>#{!Keyword.get(instance, :registrations_open)}</closed>
</site> </site>
</config> </config>
""" """
@ -157,32 +158,32 @@ def config(conn, _params) do
_ -> _ ->
data = %{ data = %{
name: Keyword.get(@instance, :name), name: Keyword.get(instance, :name),
description: Keyword.get(@instance, :description), description: Keyword.get(instance, :description),
server: Web.base_url(), server: Web.base_url(),
textlimit: to_string(Keyword.get(@instance, :limit)), textlimit: to_string(Keyword.get(instance, :limit)),
closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1"), closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"),
private: if(Keyword.get(@instance, :public, true), do: "0", else: "1") private: if(Keyword.get(instance, :public, true), do: "0", else: "1")
} }
pleroma_fe = %{ pleroma_fe = %{
theme: Keyword.get(@instance_fe, :theme), theme: Keyword.get(instance_fe, :theme),
background: Keyword.get(@instance_fe, :background), background: Keyword.get(instance_fe, :background),
logo: Keyword.get(@instance_fe, :logo), logo: Keyword.get(instance_fe, :logo),
logoMask: Keyword.get(@instance_fe, :logo_mask), logoMask: Keyword.get(instance_fe, :logo_mask),
logoMargin: Keyword.get(@instance_fe, :logo_margin), logoMargin: Keyword.get(instance_fe, :logo_margin),
redirectRootNoLogin: Keyword.get(@instance_fe, :redirect_root_no_login), redirectRootNoLogin: Keyword.get(instance_fe, :redirect_root_no_login),
redirectRootLogin: Keyword.get(@instance_fe, :redirect_root_login), redirectRootLogin: Keyword.get(instance_fe, :redirect_root_login),
chatDisabled: !Keyword.get(@instance_chat, :enabled), chatDisabled: !Keyword.get(instance_chat, :enabled),
showInstanceSpecificPanel: Keyword.get(@instance_fe, :show_instance_panel), showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel),
scopeOptionsEnabled: Keyword.get(@instance_fe, :scope_options_enabled), scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled),
formattingOptionsEnabled: Keyword.get(@instance_fe, :formatting_options_enabled), formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled),
collapseMessageWithSubject: Keyword.get(@instance_fe, :collapse_message_with_subject), collapseMessageWithSubject: Keyword.get(instance_fe, :collapse_message_with_subject),
hidePostStats: Keyword.get(@instance_fe, :hide_post_stats), hidePostStats: Keyword.get(instance_fe, :hide_post_stats),
hideUserStats: Keyword.get(@instance_fe, :hide_user_stats) hideUserStats: Keyword.get(instance_fe, :hide_user_stats)
} }
managed_config = Keyword.get(@instance, :managed_config) managed_config = Keyword.get(instance, :managed_config)
data = data =
if managed_config do if managed_config do
@ -196,7 +197,7 @@ def config(conn, _params) do
end end
def version(conn, _params) do def version(conn, _params) do
version = Keyword.get(@instance, :version) version = Pleroma.Config.get([:instance, :version])
case get_format(conn) do case get_format(conn) do
"xml" -> "xml" ->
@ -212,7 +213,7 @@ def version(conn, _params) do
end end
def emoji(conn, _params) do def emoji(conn, _params) do
json(conn, Enum.into(Formatter.get_custom_emoji(), %{})) json(conn, Enum.into(Emoji.get_all(), %{}))
end end
def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do

View file

@ -3,11 +3,10 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.{OStatus, CommonAPI} alias Pleroma.Web.{OStatus, CommonAPI}
alias Pleroma.Web.MediaProxy
import Ecto.Query import Ecto.Query
@instance Application.get_env(:pleroma, :instance)
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
@registrations_open Keyword.get(@instance, :registrations_open)
def create_status(%User{} = user, %{"status" => _} = data) do def create_status(%User{} = user, %{"status" => _} = data) do
CommonAPI.post(user, data) CommonAPI.post(user, data)
@ -20,15 +19,16 @@ def delete(%User{} = user, id) do
end end
end end
@activitypub Application.get_env(:pleroma, :activitypub)
@follow_handshake_timeout Keyword.get(@activitypub, :follow_handshake_timeout)
def follow(%User{} = follower, params) do def follow(%User{} = follower, params) do
with {:ok, %User{} = followed} <- get_user(params), with {:ok, %User{} = followed} <- get_user(params),
{:ok, follower} <- User.maybe_direct_follow(follower, followed), {:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed), {:ok, activity} <- ActivityPub.follow(follower, followed),
{:ok, 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} {:ok, follower, followed, activity}
else else
err -> err err -> err
@ -97,7 +97,7 @@ def upload(%Plug.Upload{} = file, format \\ "xml") do
{:ok, object} = ActivityPub.upload(file) {:ok, object} = ActivityPub.upload(file)
url = List.first(object.data["url"]) url = List.first(object.data["url"])
href = url["href"] href = url["href"] |> MediaProxy.url()
type = url["mediaType"] type = url["mediaType"]
case format do case format do
@ -138,18 +138,20 @@ def register_user(params) do
password_confirmation: params["confirm"] password_confirmation: params["confirm"]
} }
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
# no need to query DB if registration is open # no need to query DB if registration is open
token = token =
unless @registrations_open || is_nil(tokenString) do unless registrations_open || is_nil(tokenString) do
Repo.get_by(UserInviteToken, %{token: tokenString}) Repo.get_by(UserInviteToken, %{token: tokenString})
end end
cond do cond do
@registrations_open || (!is_nil(token) && !token.used) -> registrations_open || (!is_nil(token) && !token.used) ->
changeset = User.register_changeset(%User{}, params) changeset = User.register_changeset(%User{}, params)
with {:ok, user} <- Repo.insert(changeset) do 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} {:ok, user}
else else
{:error, changeset} -> {:error, changeset} ->
@ -160,10 +162,10 @@ def register_user(params) do
{:error, %{error: errors}} {:error, %{error: errors}}
end end
!@registrations_open && is_nil(token) -> !registrations_open && is_nil(token) ->
{:error, "Invalid token"} {:error, "Invalid token"}
!@registrations_open && token.used -> !registrations_open && token.used ->
{:error, "Expired token"} {:error, "Expired token"}
end end
end end

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
require Logger require Logger
plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
action_fallback(:errors) action_fallback(:errors)
def verify_credentials(%{assigns: %{user: user}} = conn, _params) do 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}) |> render(ActivityView, "index.json", %{activities: activities, for: user})
end 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 def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params) 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}) |> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
end 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 def follow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.follow(user, params) do case TwitterAPI.follow(user, params) do
{:ok, user, followed, _activity} -> {:ok, user, followed, _activity} ->
@ -263,7 +290,11 @@ def register(conn, params) do
end end
def update_avatar(%{assigns: %{user: user}} = conn, params) do 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}) change = Changeset.change(user, %{avatar: object.data})
{:ok, user} = User.update_and_set_cache(change) {:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user) CommonAPI.update(user)
@ -272,7 +303,11 @@ def update_avatar(%{assigns: %{user: user}} = conn, params) do
end end
def update_banner(%{assigns: %{user: user}} = conn, params) do 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), new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}), change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do {:ok, user} <- User.update_and_set_cache(change) do
@ -286,7 +321,11 @@ def update_banner(%{assigns: %{user: user}} = conn, params) do
end end
def update_background(%{assigns: %{user: user}} = conn, params) do 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), new_info <- Map.put(user.info, "background", object.data),
change <- User.info_changeset(user, %{info: new_info}), change <- User.info_changeset(user, %{info: new_info}),
{:ok, _user} <- User.update_and_set_cache(change) do {: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) json_reply(conn, 403, json)
end 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 defp error_json(conn, error_message) do
%{"error" => error_message, "request" => conn.request_path} |> Jason.encode!() %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
end end

View file

@ -283,11 +283,11 @@ def render_content(%{"type" => "Note"} = object) do
{summary, content} {summary, content}
end 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"] summary = object["name"] || object["summary"]
content = content =
if !!summary and summary != "" do if !!summary and summary != "" and is_bitstring(object["url"]) do
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}" "<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
else else
object["content"] object["content"]

View file

@ -55,8 +55,12 @@ def render("user.json", %{user: user = %User{}} = assigns) do
"statusnet_blocking" => statusnet_blocking, "statusnet_blocking" => statusnet_blocking,
"friends_count" => user_info[:following_count], "friends_count" => user_info[:following_count],
"id" => user.id, "id" => user.id,
"name" => user.name, "name" => user.name || user.nickname,
"name_html" => HTML.strip_tags(user.name) |> Formatter.emojify(emoji), "name_html" =>
if(user.name,
do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji),
else: user.nickname
),
"profile_image_url" => image, "profile_image_url" => image,
"profile_image_url_https" => image, "profile_image_url_https" => image,
"profile_image_url_profile_size" => image, "profile_image_url_profile_size" => image,

View file

@ -3,6 +3,8 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
plug(Pleroma.Web.FederatingPlug)
def host_meta(conn, _params) do def host_meta(conn, _params) do
xml = WebFinger.host_meta() xml = WebFinger.host_meta()

View file

@ -5,6 +5,15 @@ defmodule Pleroma.Web.Websub.WebsubController do
alias Pleroma.Web.Websub.WebsubClientSubscription alias Pleroma.Web.Websub.WebsubClientSubscription
require Logger 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 def websub_subscription_request(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)

18
mix.exs
View file

@ -10,7 +10,19 @@ def project do
compilers: [:phoenix, :gettext] ++ Mix.compilers(), compilers: [:phoenix, :gettext] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
aliases: aliases(), 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 end
@ -53,7 +65,9 @@ defp deps do
{:credo, "~> 0.9.3", only: [:dev, :test]}, {:credo, "~> 0.9.3", only: [:dev, :test]},
{:mock, "~> 0.3.1", only: :test}, {:mock, "~> 0.3.1", only: :test},
{:crypt, {: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 end

View file

@ -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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
@ -30,6 +32,7 @@
"mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"}, "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"}, "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"}, "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"}, "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "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"}, "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"},

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.0808aeafc6252b3050ea95b17dcaff1a.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.0a010c91e43b948ff1a3.js></script><script type=text/javascript src=/static/js/vendor.865340b21f539d34e2e1.js></script><script type=text/javascript src=/static/js/app.d429b9c3b1c1f499710c.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.0808aeafc6252b3050ea95b17dcaff1a.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.35cf9744b80c38efa9fa.js></script><script type=text/javascript src=/static/js/vendor.7e4a5b87ce584522089d.js></script><script type=text/javascript src=/static/js/app.a65abd01bcd13a691048.js></script></body></html>

View file

@ -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"
}
]
}

View file

@ -12,5 +12,6 @@
"formattingOptionsEnabled": false, "formattingOptionsEnabled": false,
"collapseMessageWithSubject": false, "collapseMessageWithSubject": false,
"hidePostStats": false, "hidePostStats": false,
"hideUserStats": false "hideUserStats": false,
"loginMethod": "password"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var r=window.webpackJsonp;window.webpackJsonp=function(c,o){for(var p,l,s=0,i=[];s<c.length;s++)l=c[s],a[l]&&i.push.apply(i,a[l]),a[l]=0;for(p in o)Object.prototype.hasOwnProperty.call(o,p)&&(e[p]=o[p]);for(r&&r(c,o);i.length;)i.shift().call(null,t);if(o[0])return n[0]=0,t(0)};var n={},a={0:0};t.e=function(e,r){if(0===a[e])return r.call(null,t);if(void 0!==a[e])a[e].push(r);else{a[e]=[r];var n=document.getElementsByTagName("head")[0],c=document.createElement("script");c.type="text/javascript",c.charset="utf-8",c.async=!0,c.src=t.p+"static/js/"+e+"."+{1:"865340b21f539d34e2e1",2:"d429b9c3b1c1f499710c"}[e]+".js",n.appendChild(c)}},t.m=e,t.c=n,t.p="/"}([]);
//# sourceMappingURL=manifest.0a010c91e43b948ff1a3.js.map

View file

@ -0,0 +1,2 @@
!function(e){function t(a){if(r[a])return r[a].exports;var n=r[a]={exports:{},id:a,loaded:!1};return e[a].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var a=window.webpackJsonp;window.webpackJsonp=function(o,c){for(var p,l,s=0,i=[];s<o.length;s++)l=o[s],n[l]&&i.push.apply(i,n[l]),n[l]=0;for(p in c)Object.prototype.hasOwnProperty.call(c,p)&&(e[p]=c[p]);for(a&&a(o,c);i.length;)i.shift().call(null,t);if(c[0])return r[0]=0,t(0)};var r={},n={0:0};t.e=function(e,a){if(0===n[e])return a.call(null,t);if(void 0!==n[e])n[e].push(a);else{n[e]=[a];var r=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=t.p+"static/js/"+e+"."+{1:"7e4a5b87ce584522089d",2:"a65abd01bcd13a691048"}[e]+".js",r.appendChild(o)}},t.m=e,t.c=r,t.p="/"}([]);
//# sourceMappingURL=manifest.35cf9744b80c38efa9fa.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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"]
]

View file

@ -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"]
]

View file

@ -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"]
]

56
test/config_test.exs Normal file
View file

@ -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

View file

@ -5,19 +5,88 @@ defmodule Pleroma.FilterTest do
import Pleroma.Factory import Pleroma.Factory
import Ecto.Query import Ecto.Query
test "creating a filter" do describe "creating filters" do
user = insert(:user) test "creating one filter" do
user = insert(:user)
query = %Pleroma.Filter{ query = %Pleroma.Filter{
user_id: user.id, user_id: user.id,
filter_id: 42, filter_id: 42,
phrase: "knights", phrase: "knights",
context: ["home"] context: ["home"]
} }
{:ok, %Pleroma.Filter{} = filter} = Pleroma.Filter.create(query) {:ok, %Pleroma.Filter{} = filter} = Pleroma.Filter.create(query)
result = Pleroma.Filter.get(filter.filter_id, user) result = Pleroma.Filter.get(filter.filter_id, user)
assert query.phrase == result.phrase 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 end
test "deleting a filter" do test "deleting a filter" do

View file

@ -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"}}]}

65
test/fixtures/prismo-url-map.json vendored Normal file
View file

@ -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"
}
}
]
}
}

View file

@ -3,6 +3,7 @@ defmodule Pleroma.NotificationTest do
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.{User, Notification} alias Pleroma.{User, Notification}
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory import Pleroma.Factory
describe "create_notifications" do describe "create_notifications" do
@ -121,6 +122,135 @@ test "it clears all notifications belonging to the user" do
end end
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 describe "notification lifecycle" do
test "liking an activity results in 1 notification, then 0 if the activity is deleted" do test "liking an activity results in 1 notification, then 0 if the activity is deleted" do
user = insert(:user) user = insert(:user)

View file

@ -19,4 +19,34 @@ test "it ensures uniqueness of the id" do
{:error, _result} = Repo.insert(cs) {:error, _result} = Repo.insert(cs)
end end
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 end

View file

@ -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

View file

@ -3,6 +3,14 @@ defmodule HTTPoisonMock do
def get(url, body \\ [], headers \\ []) 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 def get("https://osada.macgirvin.com/channel/mike", _, _) do
{:ok, {:ok,
%Response{ %Response{

View file

@ -487,11 +487,13 @@ test "get recipients from activity" do
assert addressed in recipients assert addressed in recipients
end end
test ".deactivate deactivates a user" do test ".deactivate can de-activate then re-activate a user" do
user = insert(:user) user = insert(:user)
assert false == !!user.info["deactivated"] assert false == !!user.info["deactivated"]
{:ok, user} = User.deactivate(user) {:ok, user} = User.deactivate(user)
assert true == user.info["deactivated"] assert true == user.info["deactivated"]
{:ok, user} = User.deactivate(user, false)
assert false == !!user.info["deactivated"]
end end
test ".delete deactivates a user, all follow relationships and all create activities" do 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.favorite(activity.id, follower)
{:ok, _, _} = CommonAPI.repeat(activity.id, follower) {:ok, _, _} = CommonAPI.repeat(activity.id, follower)
:ok = User.delete(user) {:ok, _} = User.delete(user)
followed = Repo.get(User, followed.id) followed = Repo.get(User, followed.id)
follower = Repo.get(User, follower.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) assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user)
end end
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 end

View file

@ -5,6 +5,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
alias Pleroma.{Repo, User} alias Pleroma.{Repo, User}
alias Pleroma.Activity 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 describe "/users/:nickname" do
test "it returns a json representation of the user", %{conn: conn} do test "it returns a json representation of the user", %{conn: conn} do
user = insert(:user) user = insert(:user)

View file

@ -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

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