Browse Source

Merge branch 'develop' into feature/compat/push-subscriptions

# Conflicts:
#	lib/mix/tasks/sample_config.eex
#	lib/pleroma/web/twitter_api/controllers/util_controller.ex
#	mix.exs
#	mix.lock
tags/v0.9.9
Egor Kislitsyn 5 years ago
parent
commit
8b4397c704
100 changed files with 2718 additions and 1019 deletions
  1. +6
    -1
      .gitlab-ci.yml
  2. +0
    -106
      CONFIGURATION.md
  3. +12
    -21
      README.md
  4. +62
    -13
      config/config.exs
  5. +102
    -0
      config/config.md
  6. +8
    -24
      installation/caddyfile-pleroma.example
  7. +20
    -22
      installation/pleroma-apache.conf
  8. +2
    -24
      installation/pleroma.nginx
  9. +11
    -0
      installation/pleroma.service
  10. +0
    -10
      installation/pleroma.vcl
  11. +7
    -1
      lib/mix/tasks/deactivate_user.ex
  12. +9
    -1
      lib/mix/tasks/generate_config.ex
  13. +8
    -1
      lib/mix/tasks/generate_invite_token.ex
  14. +7
    -1
      lib/mix/tasks/generate_password_reset.ex
  15. +8
    -1
      lib/mix/tasks/make_moderator.ex
  16. +19
    -0
      lib/mix/tasks/reactivate_user.ex
  17. +8
    -0
      lib/mix/tasks/register_user.ex
  18. +13
    -4
      lib/mix/tasks/relay_follow.ex
  19. +13
    -5
      lib/mix/tasks/relay_unfollow.ex
  20. +8
    -2
      lib/mix/tasks/rm_user.ex
  21. +7
    -3
      lib/mix/tasks/sample_config.eex
  22. +2
    -4
      lib/mix/tasks/sample_psql.eex
  23. +32
    -0
      lib/mix/tasks/set_admin.ex
  24. +10
    -1
      lib/mix/tasks/set_locked.ex
  25. +38
    -0
      lib/mix/tasks/unsubscribe_user.ex
  26. +6
    -0
      lib/pleroma/activity.ex
  27. +33
    -8
      lib/pleroma/application.ex
  28. +42
    -0
      lib/pleroma/config.ex
  29. +194
    -0
      lib/pleroma/emoji.ex
  30. +28
    -0
      lib/pleroma/filter.ex
  31. +20
    -150
      lib/pleroma/formatter.ex
  32. +6
    -9
      lib/pleroma/gopher/server.ex
  33. +16
    -10
      lib/pleroma/html.ex
  34. +1
    -0
      lib/pleroma/http/http.ex
  35. +19
    -0
      lib/pleroma/list.ex
  36. +76
    -2
      lib/pleroma/notification.ex
  37. +15
    -5
      lib/pleroma/object.ex
  38. +18
    -0
      lib/pleroma/plugs/federating_plug.ex
  39. +63
    -0
      lib/pleroma/plugs/http_security_plug.ex
  40. +19
    -0
      lib/pleroma/plugs/user_is_admin_plug.ex
  41. +71
    -51
      lib/pleroma/upload.ex
  42. +26
    -0
      lib/pleroma/uploaders/mdii.ex
  43. +18
    -2
      lib/pleroma/uploaders/s3.ex
  44. +5
    -6
      lib/pleroma/uploaders/swift/keystone.ex
  45. +2
    -4
      lib/pleroma/uploaders/swift/swift.ex
  46. +57
    -52
      lib/pleroma/user.ex
  47. +71
    -26
      lib/pleroma/web/activity_pub/activity_pub.ex
  48. +41
    -8
      lib/pleroma/web/activity_pub/activity_pub_controller.ex
  49. +1
    -3
      lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
  50. +4
    -6
      lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
  51. +51
    -37
      lib/pleroma/web/activity_pub/mrf/simple_policy.ex
  52. +23
    -0
      lib/pleroma/web/activity_pub/mrf/user_allowlist.ex
  53. +8
    -6
      lib/pleroma/web/activity_pub/relay.ex
  54. +168
    -62
      lib/pleroma/web/activity_pub/transmogrifier.ex
  55. +69
    -16
      lib/pleroma/web/activity_pub/utils.ex
  56. +25
    -18
      lib/pleroma/web/activity_pub/views/object_view.ex
  57. +1
    -1
      lib/pleroma/web/activity_pub/views/user_view.ex
  58. +158
    -0
      lib/pleroma/web/admin_api/admin_api_controller.ex
  59. +3
    -4
      lib/pleroma/web/channels/user_socket.ex
  60. +19
    -6
      lib/pleroma/web/common_api/common_api.ex
  61. +51
    -12
      lib/pleroma/web/common_api/utils.ex
  62. +13
    -6
      lib/pleroma/web/endpoint.ex
  63. +44
    -36
      lib/pleroma/web/federator/federator.ex
  64. +71
    -0
      lib/pleroma/web/federator/retry_queue.ex
  65. +102
    -48
      lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
  66. +27
    -6
      lib/pleroma/web/mastodon_api/mastodon_socket.ex
  67. +10
    -1
      lib/pleroma/web/mastodon_api/views/account_view.ex
  68. +14
    -9
      lib/pleroma/web/mastodon_api/views/status_view.ex
  69. +41
    -3
      lib/pleroma/web/media_proxy/controller.ex
  70. +6
    -1
      lib/pleroma/web/media_proxy/media_proxy.ex
  71. +68
    -5
      lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
  72. +9
    -1
      lib/pleroma/web/oauth/authorization.ex
  73. +32
    -19
      lib/pleroma/web/oauth/oauth_controller.ex
  74. +10
    -0
      lib/pleroma/web/oauth/token.ex
  75. +15
    -0
      lib/pleroma/web/ostatus/ostatus.ex
  76. +16
    -4
      lib/pleroma/web/ostatus/ostatus_controller.ex
  77. +70
    -45
      lib/pleroma/web/router.ex
  78. +33
    -11
      lib/pleroma/web/streamer.ex
  79. +3
    -1
      lib/pleroma/web/templates/layout/app.html.eex
  80. +0
    -11
      lib/pleroma/web/templates/mastodon_api/mastodon/login.html.eex
  81. +31
    -27
      lib/pleroma/web/twitter_api/controllers/util_controller.ex
  82. +8
    -0
      lib/pleroma/web/twitter_api/representers/activity_representer.ex
  83. +16
    -9
      lib/pleroma/web/twitter_api/twitter_api.ex
  84. +65
    -5
      lib/pleroma/web/twitter_api/twitter_api_controller.ex
  85. +10
    -2
      lib/pleroma/web/twitter_api/views/activity_view.ex
  86. +15
    -3
      lib/pleroma/web/twitter_api/views/user_view.ex
  87. +2
    -0
      lib/pleroma/web/web_finger/web_finger_controller.ex
  88. +25
    -0
      lib/pleroma/web/websub/websub.ex
  89. +9
    -0
      lib/pleroma/web/websub/websub_controller.ex
  90. +64
    -2
      mix.exs
  91. +6
    -0
      mix.lock
  92. +1
    -1
      priv/static/index.html
  93. +23
    -0
      priv/static/schemas/litepub-0.1.jsonld
  94. +4
    -1
      priv/static/static/config.json
  95. +11
    -0
      priv/static/static/js/app.065638d22ade92dea420.js
  96. +1
    -0
      priv/static/static/js/app.065638d22ade92dea420.js.map
  97. +0
    -9
      priv/static/static/js/app.daf013e442326e175355.js
  98. +0
    -1
      priv/static/static/js/app.daf013e442326e175355.js.map
  99. +2
    -2
      priv/static/static/js/manifest.34667c2817916147413f.js
  100. +1
    -1
      priv/static/static/js/manifest.34667c2817916147413f.js.map

+ 6
- 1
.gitlab-ci.yml View File

@@ -1,4 +1,4 @@
image: elixir:1.6.4
image: elixir:1.7.2

services:
- postgres:9.6.2
@@ -9,6 +9,11 @@ variables:
POSTGRES_PASSWORD: postgres
DB_HOST: postgres

cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- deps
- _build
stages:
- lint
- test


+ 0
- 106
CONFIGURATION.md 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

+ 12
- 21
README.md View File

@@ -2,11 +2,13 @@

## About Pleroma

Pleroma is an OStatus-compatible social networking server written in Elixir, compatible with GNU Social and Mastodon. It is high-performance and can run on small devices like a Raspberry Pi.
Pleroma is a microblogging server software that can federate (= exchange messages with) other servers that support the same federation standards (OStatus and ActivityPub). What that means is that you can host a server for yourself or your friends and stay in control of your online identity, but still exchange messages with people on larger servers. Pleroma will federate with all servers that implement either OStatus or ActivityPub, like Friendica, GNU Social, Hubzilla, Mastodon, Misskey, Peertube, and Pixelfed.

Pleroma is written in Elixir, high-performance and can run on small devices like a Raspberry Pi.

For clients it supports both the [GNU Social API with Qvitter extensions](https://twitter-api.readthedocs.io/en/latest/index.html) and the [Mastodon client API](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md).

Mobile clients that are known to work well:
Client applications that are known to work well:

* Twidere
* Tusky
@@ -15,6 +17,7 @@ Mobile clients that are known to work well:
* Amaroq (iOS)
* Tootdon (Android + iOS)
* Tootle (iOS)
* Whalebird (Windows + Mac + Linux)

No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org.

@@ -36,7 +39,7 @@ While we don't provide docker files, other people have written very good ones. T

* Run `mix generate_config`. This will ask you a few questions about your instance and generate a configuration file in `config/generated_config.exs`. Check that and copy it to either `config/dev.secret.exs` or `config/prod.secret.exs`. It will also create a `config/setup_db.psql`; you may want to double-check this file in case you wanted a different username, or database name than the default. Then you need to run the script as PostgreSQL superuser (i.e. `sudo su postgres -c "psql -f config/setup_db.psql"`). It will create a pleroma db user, database and will setup needed extensions that need to be set up. Postgresql super-user privileges are only needed for this step.

* For these next steps, the default will be to run pleroma using the dev configuration file, `config/dev.secret.exs`. To run them using the prod config file, prefix each command at the shell with `MIX_ENV=prod`. For example: `MIX_ENV=prod mix phx.server`.
* For these next steps, the default will be to run pleroma using the dev configuration file, `config/dev.secret.exs`. To run them using the prod config file, prefix each command at the shell with `MIX_ENV=prod`. For example: `MIX_ENV=prod mix phx.server`. Documentation for the config can be found at [``config/config.md``](config/config.md)

* Run `mix ecto.migrate` to run the database migrations. You will have to do this again after certain updates.

@@ -45,8 +48,6 @@ While we don't provide docker files, other people have written very good ones. T
* The common and convenient way for adding HTTPS is by using Nginx as a reverse proxy. You can look at example Nginx configuration in `installation/pleroma.nginx`. If you need TLS/SSL certificates for HTTPS, you can look get some for free with letsencrypt: https://letsencrypt.org/
The simplest way to obtain and install a certificate is to use [Certbot.](https://certbot.eff.org) Depending on your specific setup, certbot may be able to get a certificate and configure your web server automatically.

* [Not tested with system reboot yet!] You'll also want to set up Pleroma to be run as a systemd service. Example .service file can be found in `installation/pleroma.service` you can put it in `/etc/systemd/system/`.

## Running

* By default, it listens on port 4000 (TCP), so you can access it on http://localhost:4000/ (if you are on the same machine). In case of an error it will restart automatically.
@@ -55,9 +56,15 @@ While we don't provide docker files, other people have written very good ones. T
Pleroma comes with two frontends. The first one, Pleroma FE, can be reached by normally visiting the site. The other one, based on the Mastodon project, can be found by visiting the /web path of your site.

### As systemd service (with provided .service file)
Example .service file can be found in `installation/pleroma.service` you can put it in `/etc/systemd/system/`.
Running `service pleroma start`
Logs can be watched by using `journalctl -fu pleroma.service`

### As OpenRC service (with provided RC file)
Copy ``installation/init.d/pleroma`` to ``/etc/init.d/pleroma``.
You can add it to the services ran by default with:
``rc-update add pleroma``

### Standalone/run by other means
Run `mix phx.server` in repository's root, it will output log into stdout/stderr

@@ -70,22 +77,6 @@ Add the following to your `dev.secret.exs` or `prod.secret.exs` if you want to p

This is useful for running pleroma inside Tor or i2p.

## Admin Tasks

### Register a User

Run `mix register_user <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

### No incoming federation


+ 62
- 13
config/config.exs View File

@@ -20,17 +20,39 @@ config :pleroma, Pleroma.Uploaders.Local,

config :pleroma, Pleroma.Uploaders.S3,
bucket: nil,
public_endpoint: "https://s3.amazonaws.com"
public_endpoint: "https://s3.amazonaws.com",
force_media_proxy: false

config :pleroma, Pleroma.Uploaders.MDII,
cgi: "https://mdii.sakura.ne.jp/mdii-post.cgi",
files: "https://mdii.sakura.ne.jp"

config :pleroma, :emoji, shortcode_globs: ["/emoji/custom/**/*.png"]

config :pleroma, :uri_schemes, additionnal_schemes: []
config :pleroma, :uri_schemes,
valid_schemes: [
"https",
"http",
"dat",
"dweb",
"gopher",
"ipfs",
"ipns",
"irc",
"ircs",
"magnet",
"mailto",
"mumble",
"ssb",
"xmpp"
]

# Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "localhost"],
protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
signing_salt: "CqaoopA2",
render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
pubsub: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2],
secure_cookie_flag: true
@@ -51,30 +73,32 @@ config :pleroma, :websub, Pleroma.Web.Websub
config :pleroma, :ostatus, Pleroma.Web.OStatus
config :pleroma, :httpoison, Pleroma.HTTP

version =
with {version, 0} <- System.cmd("git", ["rev-parse", "HEAD"]) do
"Pleroma #{Mix.Project.config()[:version]} #{String.trim(version)}"
else
_ -> "Pleroma #{Mix.Project.config()[:version]} dev"
end

# Configures http settings, upstream proxy etc.
config :pleroma, :http, proxy_url: nil

config :pleroma, :instance,
version: version,
name: "Pleroma",
email: "example@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
registrations_open: true,
federating: true,
allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true,
quarantined_instances: [],
managed_config: true
managed_config: true,
allowed_post_formats: [
"text/plain",
"text/html",
"text/markdown"
],
finmoji_enabled: true,
mrf_transparency: true

config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because
@@ -98,12 +122,16 @@ config :pleroma, :fe,
redirect_root_login: "/main/friends",
show_instance_panel: true,
scope_options_enabled: false,
collapse_message_with_subject: false
formatting_options_enabled: false,
collapse_message_with_subject: false,
hide_post_stats: false,
hide_user_stats: false

config :pleroma, :activitypub,
accept_blocks: true,
unfollow_blocked: true,
outgoing_blocks: true
outgoing_blocks: true,
follow_handshake_timeout: 500

config :pleroma, :user, deny_follow_blocked: true

@@ -145,6 +173,27 @@ config :pleroma, :suggestions,
limit: 23,
web: "https://vinayaka.distsn.org/?{{host}}+{{user}}"

config :pleroma, :http_security,
enabled: true,
sts: false,
sts_max_age: 31_536_000,
ct_max_age: 2_592_000,
referrer_policy: "same-origin"

config :cors_plug,
max_age: 86_400,
methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"],
expose: [
"Link",
"X-RateLimit-Reset",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-Request-Id",
"Idempotency-Key"
],
credentials: true,
headers: ["Authorization", "Content-Type", "Idempotency-Key"]

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

+ 102
- 0
config/config.md View File

@@ -0,0 +1,102 @@
# Configuration

This file describe the configuration, it is recommended to edit the relevant *.secret.exs file instead of the others founds in the ``config`` directory.
If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherwise it is ``dev.secret.exs``.

## Pleroma.Upload
* `uploader`: Select which `Pleroma.Uploaders` to use
* `strip_exif`: boolean, uses ImageMagick(!) to strip exif.

## Pleroma.Uploaders.Local
* `uploads`: Which directory to store the user-uploads in, relative to pleroma’s working directory
* `uploads_url`: The URL to access a user-uploaded file, ``{{base_url}}`` is replaced to the instance URL and ``{{file}}`` to the filename. Useful when you want to proxy the media files via another host.

## :uri_schemes
* `valid_schemes`: List of the scheme part that is considered valid to be an URL

## :instance
* `name`: The instance’s name
* `email`: Email used to reach an Administrator/Moderator of the instance
* `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``
* `limit`: Posts character limit (CW/Subject included in the counter)
* `upload_limit`: File size limit of uploads (except for avatar, background, banner)
* `avatar_upload_limit`: File size limit of user’s profile avatars
* `background_upload_limit`: File size limit of user’s profile backgrounds
* `banner_upload_limit`: File size limit of user’s profile backgrounds
* `registerations_open`: Enable registerations for anyone, invitations can be used when false.
* `federating`
* `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance
* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default)
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See ``:mrf_simple`` section)
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See ``:mrf_rejectnonpublic`` section)
* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
* `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json``
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML)
* `finmoji_enabled`: Whenether to enable the finmojis in the custom emojis.
* `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).

## :fe
This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false.

* `theme`: Which theme to use, they are defined in ``styles.json``
* `logo`: URL of the logo, defaults to Pleroma’s logo
* `logo_mask`: Whenether to mask the logo
* `logo_margin`: What margin to use around the logo
* `background`: URL of the background, unless viewing a user profile with a background that is set
* `redirect_root_no_login`: relative URL which indicates where to redirect when a user isn’t logged in.
* `redirect_root_login`: relative URL which indicates where to redirect when a user is logged in.
* `show_instance_panel`: Whenether to show the instance’s specific panel.
* `scope_options_enabled`: Enable setting an notice visibility and subject/CW when posting
* `formatting_options_enabled`: Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to ``:instance, allowed_post_formats``
* `collapse_message_with_subjects`: When a message has a subject(aka Content Warning), collapse it by default
* `hide_post_stats`: Hide notices statistics(repeats, favorites, …)
* `hide_user_stats`: Hide profile statistics(posts, posts per day, followers, followings, …)

## :mrf_simple
* `media_removal`: List of instances to remove medias from
* `media_nsfw`: List of instances to put medias as NSFW(sensitive) from
* `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline
* `reject`: List of instances to reject any activities from
* `accept`: List of instances to accept any activities from

## :mrf_rejectnonpublic
* `allow_followersonly`: whether to allow followers-only posts
* `allow_direct`: whether to allow direct messages

## :media_proxy
* `enabled`: Enables proxying of remote media to the instance’s proxy
* `redirect_on_failure`: Use the original URL when Media Proxy fails to get it

## :gopher
* `enabled`: Enables the gopher interface
* `ip`: IP address to bind to
* `port`: Port to bind to

## :activitypub
* ``accept_blocks``: Whether to accept incoming block activities from other instances
* ``unfollow_blocked``: Whether blocks result in people getting unfollowed
* ``outgoing_blocks``: Whether to federate blocks to other instances
* ``deny_follow_blocked``: Whether to disallow following an account that has blocked the user in question

## :http_security
* ``enabled``: Whether the managed content security policy is enabled
* ``sts``: Whether to additionally send a `Strict-Transport-Security` header
* ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent
* ``ct_max_age``: The maximum age for the `Expect-CT` header if sent
* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`.

## :mrf_user_allowlist

The keys in this section are the domain names that the policy should apply to.
Each key should be assigned a list of users that should be allowed through by
their ActivityPub ID.

An example:

```
config :pleroma, :mrf_user_allowlist,
"example.org": ["https://example.org/users/admin"]
```

+ 8
- 24
installation/caddyfile-pleroma.example 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
errors /var/log/caddy/pleroma_error.log

@@ -9,34 +15,12 @@ social.domain.tld {
transparent
}

tls user@domain.tld {
tls {
# Remove the rest of the lines in here, if you want to support older devices
key_type p256
ciphers ECDHE-ECDSA-WITH-CHACHA20-POLY1305 ECDHE-RSA-WITH-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256
}

header / {
X-XSS-Protection "1; mode=block"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "same-origin"
Strict-Transport-Security "max-age=31536000; includeSubDomains;"
Expect-CT "enforce, max-age=2592000"
Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://social.domain.tld; upgrade-insecure-requests;"
}

# If you do not want remote frontends to be able to access your Pleroma backend server, remove these lines.
# If you want to allow all origins access, remove the origin lines.
# To use this directive, you need the http.cors plugin for Caddy.
cors / {
origin https://halcyon.domain.tld
origin https://pinafore.domain.tld
methods POST,PUT,DELETE,GET,PATCH,OPTIONS
allowed_headers Authorization,Content-Type,Idempotency-Key
exposed_headers Link,X-RateLimit-Reset,X-RateLimit-Limit,X-RateLimit-Remaining,X-Request-Id
}
# Stop removing lines here.

# If you do not want to use the mediaproxy function, remove these lines.
# To use this directive, you need the http.cache plugin for Caddy.
cache {


+ 20
- 22
installation/pleroma-apache.conf View File

@@ -1,24 +1,31 @@
#Example configuration for when Apache httpd and Pleroma are on the same host.
#Needed modules: headers proxy proxy_http proxy_wstunnel rewrite ssl
#This assumes a Debian style Apache config. Put this in /etc/apache2/sites-available
#Install your TLS certificate, possibly using Let's Encrypt.
#Replace 'pleroma.example.com' with your instance's domain wherever it appears

ServerName pleroma.example.com
# default Apache site config for Pleroma
#
# needed modules: define headers proxy proxy_http proxy_wstunnel rewrite ssl
#
# Simple installation instructions:
# 1. Install your TLS certificate, possibly using Let's Encrypt.
# 2. Replace 'example.tld' with your instance's domain wherever it appears.
# 3. This assumes a Debian style Apache config. Copy this file to
# /etc/apache2/sites-available/ and then add a symlink to it in
# /etc/apache2/sites-enabled/ by running 'a2ensite pleroma-apache.conf', then restart Apache.

Define servername example.tld

ServerName ${servername}
ServerTokens Prod

ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined

<VirtualHost *:80>
Redirect permanent / https://pleroma.example.com
Redirect permanent / https://${servername}
</VirtualHost>

<VirtualHost *:443>
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/pleroma.example.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/pleroma.example.com/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/pleroma.example.com/fullchain.pem
SSLCertificateFile /etc/letsencrypt/live/${servername}/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/${servername}/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/${servername}/fullchain.pem

# Mozilla modern configuration, tweak to your needs
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
@@ -27,15 +34,6 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLCompression off
SSLSessionTickets off

Header always set X-Xss-Protection "1; mode=block"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy same-origin
Header always set Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://pleroma.example.tld; upgrade-insecure-requests;"

# Uncomment this only after you get HTTPS working.
# Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

RewriteEngine On
RewriteCond %{HTTP:Connection} Upgrade [NC]
RewriteCond %{HTTP:Upgrade} websocket [NC]
@@ -45,7 +43,7 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
ProxyPass / http://localhost:4000/
ProxyPassReverse / http://localhost:4000/

RequestHeader set Host "pleroma.example.com"
RequestHeader set Host ${servername}
ProxyPreserveHost On
</VirtualHost>

@@ -53,4 +51,4 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLUseStapling on
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off
SSLStaplingCache shmcb:/var/run/ocsp(128000)
SSLStaplingCache shmcb:/var/run/ocsp(128000)

+ 2
- 24
installation/pleroma.nginx 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;

server {
listen 80;
server_name example.tld;
listen 80;
return 301 https://$server_name$request_uri;

# Uncomment this if you need to use the 'webroot' method with certbot. Make sure
@@ -46,7 +46,7 @@ server {
ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;
ssl_stapling on;
ssl_stapling_verify on;
server_name example.tld;

gzip_vary on;
@@ -60,28 +60,6 @@ server {
client_max_body_size 16m;

location / {
# if you do not want remote frontends to be able to access your Pleroma backend
# server, remove these lines.
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always;
add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always;
if ($request_method = OPTIONS) {
return 204;
}
# stop removing lines here.

add_header X-XSS-Protection "1; mode=block" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "same-origin" always;
add_header X-Download-Options "noopen" always;
add_header Content-Security-Policy "default-src 'none'; base-uri 'self'; form-action '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://example.tld; upgrade-insecure-requests;" always;
# Uncomment this only after you get HTTPS working.
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";


+ 11
- 0
installation/pleroma.service View File

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

; Some security directives.
; Use private /tmp and /var/tmp folders inside a new file system namespace, which are discarded after the process stops.
PrivateTmp=true
; Mount /usr, /boot, and /etc as read-only for processes invoked by this service.
ProtectSystem=full
; Sets up a new /dev mount for the process and only adds API pseudo devices like /dev/null, /dev/zero or /dev/random but not physical devices. Disabled by default because it may not work on devices like the Raspberry Pi.
PrivateDevices=false
; Ensures that the service process and all its children can never gain new privileges through execve().
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

+ 0
- 10
installation/pleroma.vcl View File

@@ -119,13 +119,3 @@ sub vcl_pipe {
set bereq.http.connection = req.http.connection;
}
}

sub vcl_deliver {
set resp.http.X-Frame-Options = "DENY";
set resp.http.X-XSS-Protection = "1; mode=block";
set resp.http.X-Content-Type-Options = "nosniff";
set resp.http.Referrer-Policy = "same-origin";
set resp.http.Content-Security-Policy = "default-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://" + req.http.host + "; upgrade-insecure-requests;";
# Uncomment this only after you get HTTPS working.
# set resp.http.Strict-Transport-Security= "max-age=31536000; includeSubDomains";
}

+ 7
- 1
lib/mix/tasks/deactivate_user.ex View File

@@ -2,7 +2,13 @@ defmodule Mix.Tasks.DeactivateUser do
use Mix.Task
alias Pleroma.User

@shortdoc "Toggle deactivation status for a user"
@moduledoc """
Deactivates a user (local or remote)

Usage: ``mix deactivate_user <nickname>``

Example: ``mix deactivate_user lain``
"""
def run([nickname]) do
Mix.Task.run("app.start")



+ 9
- 1
lib/mix/tasks/generate_config.ex View File

@@ -1,7 +1,15 @@
defmodule Mix.Tasks.GenerateConfig do
use Mix.Task

@shortdoc "Generates a new config"
@moduledoc """
Generate a new config

## Usage
``mix generate_config``

This mix task is interactive, and will overwrite the config present at ``config/generated_config.exs``.
"""

def run(_) do
IO.puts("Answer a few questions to generate a new config\n")
IO.puts("--- THIS WILL OVERWRITE YOUR config/generated_config.exs! ---\n")


+ 8
- 1
lib/mix/tasks/generate_invite_token.ex View File

@@ -1,7 +1,14 @@
defmodule Mix.Tasks.GenerateInviteToken do
use Mix.Task

@shortdoc "Generate invite token for user"
@moduledoc """
Generates invite token

This is in the form of a URL to be used by the Invited user to register themselves.

## Usage
``mix generate_invite_token``
"""
def run([]) do
Mix.Task.run("app.start")



+ 7
- 1
lib/mix/tasks/generate_password_reset.ex View File

@@ -2,7 +2,13 @@ defmodule Mix.Tasks.GeneratePasswordReset do
use Mix.Task
alias Pleroma.User

@shortdoc "Generate password reset link for user"
@moduledoc """
Generate password reset link for user

Usage: ``mix generate_password_reset <nickname>``

Example: ``mix generate_password_reset lain``
"""
def run([nickname]) do
Mix.Task.run("app.start")



+ 8
- 1
lib/mix/tasks/make_moderator.ex View File

@@ -1,9 +1,16 @@
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
import Mix.Ecto
alias Pleroma.{Repo, User}

@shortdoc "Set moderator status"
def run([nickname | rest]) do
Application.ensure_all_started(:pleroma)



+ 19
- 0
lib/mix/tasks/reactivate_user.ex 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

+ 8
- 0
lib/mix/tasks/register_user.ex View File

@@ -1,4 +1,12 @@
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
alias Pleroma.{Repo, User}



+ 13
- 4
lib/mix/tasks/relay_follow.ex View File

@@ -4,12 +4,21 @@ defmodule Mix.Tasks.RelayFollow do
alias Pleroma.Web.ActivityPub.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
Mix.Task.run("app.start")

:ok = Relay.follow(target)

# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
with {:ok, activity} <- Relay.follow(target) do
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
else
{:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}")
end
end
end

+ 13
- 5
lib/mix/tasks/relay_unfollow.ex View File

@@ -3,13 +3,21 @@ defmodule Mix.Tasks.RelayUnfollow do
require Logger
alias Pleroma.Web.ActivityPub.Relay

@shortdoc "Follows a remote relay"
@moduledoc """
Unfollows a remote relay

Usage: ``mix relay_follow <relay_url>``

Example: ``mix relay_follow https://example.org/relay``
"""
def run([target]) do
Mix.Task.run("app.start")

:ok = Relay.unfollow(target)

# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
with {:ok, activity} <- Relay.follow(target) do
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
else
{:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}")
end
end
end

+ 8
- 2
lib/mix/tasks/rm_user.ex View File

@@ -2,12 +2,18 @@ defmodule Mix.Tasks.RmUser do
use Mix.Task
alias Pleroma.User

@shortdoc "Permanently delete a user"
@moduledoc """
Permanently deletes a user

Usage: ``mix rm_user [nickname]``

Example: ``mix rm_user lain``
"""
def run([nickname]) do
Mix.Task.run("app.start")

with %User{local: true} = user <- User.get_by_nickname(nickname) do
User.delete(user)
{:ok, _} = User.delete(user)
end
end
end

+ 7
- 3
lib/mix/tasks/sample_config.eex View File

@@ -31,6 +31,10 @@ config :web_push_encryption, :vapid_details,
public_key: "<%= web_push_public_key %>",
private_key: "<%= web_push_private_key %>"

# Enable Strict-Transport-Security once SSL is working:
# config :pleroma, :http_security,
# sts: true

# Configure S3 support if desired.
# The public S3 endpoint is different depending on region and provider,
# consult your S3 provider's documentation for details on what to use.
@@ -52,9 +56,9 @@ config :web_push_encryption, :vapid_details,


# Configure Openstack Swift support if desired.
#
# Many openstack deployments are different, so config is left very open with
# no assumptions made on which provider you're using. This should allow very
#
# Many openstack deployments are different, so config is left very open with
# no assumptions made on which provider you're using. This should allow very
# wide support without needing separate handlers for OVH, Rackspace, etc.
#
# config :pleroma, Pleroma.Uploaders.Swift,


+ 2
- 4
lib/mix/tasks/sample_psql.eex View File

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


+ 32
- 0
lib/mix/tasks/set_admin.ex View File

@@ -0,0 +1,32 @@
defmodule Mix.Tasks.SetAdmin do
use Mix.Task
alias Pleroma.User

@doc """
Sets admin status
Usage: set_admin nickname [true|false]
"""
def run([nickname | rest]) do
Application.ensure_all_started(:pleroma)

status =
case rest do
[status] -> status == "true"
_ -> true
end

with %User{local: true} = user <- User.get_by_nickname(nickname) do
info =
user.info
|> Map.put("is_admin", !!status)

cng = User.info_changeset(user, %{info: info})
{:ok, user} = User.update_and_set_cache(cng)

IO.puts("Admin status of #{nickname}: #{user.info["is_admin"]}")
else
_ ->
IO.puts("No local user #{nickname}")
end
end
end

+ 10
- 1
lib/mix/tasks/set_locked.ex View File

@@ -1,9 +1,18 @@
defmodule Mix.Tasks.SetLocked do
@moduledoc """
Lock a local user

The local user will then have to manually accept/reject followers. This can also be done by the user into their settings.

Usage: ``mix set_locked <username>``

Example: ``mix set_locked lain``
"""

use Mix.Task
import Mix.Ecto
alias Pleroma.{Repo, User}

@shortdoc "Set locked status"
def run([nickname | rest]) do
ensure_started(Repo, [])



+ 38
- 0
lib/mix/tasks/unsubscribe_user.ex 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

+ 6
- 0
lib/pleroma/activity.ex View File

@@ -82,4 +82,10 @@ defmodule Pleroma.Activity do
def normalize(obj) when is_map(obj), do: Activity.get_by_ap_id(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: Activity.get_by_ap_id(ap_id)
def normalize(_), do: nil

def get_in_reply_to_activity(%Activity{data: %{"object" => %{"inReplyTo" => ap_id}}}) do
get_create_activity_by_object_ap_id(ap_id)
end

def get_in_reply_to_activity(_), do: nil
end

+ 33
- 8
lib/pleroma/application.ex View File

@@ -1,8 +1,15 @@
defmodule Pleroma.Application do
use Application

@name "Pleroma"
@version Mix.Project.config()[:version]
def name, do: @name
def version, do: @version
def named_version(), do: @name <> " " <> @version

# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
@env Mix.env()
def start(_type, _args) do
import Supervisor.Spec
import Cachex.Spec
@@ -12,18 +19,35 @@ defmodule Pleroma.Application do
[
# Start the Ecto repository
supervisor(Pleroma.Repo, []),
worker(Pleroma.Emoji, []),
# Start the endpoint when the application starts
supervisor(Pleroma.Web.Endpoint, []),
# Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3)
# worker(Pleroma.Worker, [arg1, arg2, arg3]),
worker(Cachex, [
:user_cache,
worker(
Cachex,
[
:user_cache,
[
default_ttl: 25000,
ttl_interval: 1000,
limit: 2500
]
],
id: :cachex_user
),
worker(
Cachex,
[
default_ttl: 25000,
ttl_interval: 1000,
limit: 2500
]
]),
:object_cache,
[
default_ttl: 25000,
ttl_interval: 1000,
limit: 2500
]
],
id: :cachex_object
),
worker(
Cachex,
[
@@ -40,11 +64,12 @@ defmodule Pleroma.Application do
id: :cachex_idem
),
worker(Pleroma.Web.Federator, []),
worker(Pleroma.Web.Federator.RetryQueue, []),
worker(Pleroma.Gopher.Server, []),
worker(Pleroma.Stats, []),
worker(Pleroma.Web.Push, [])
] ++
if Mix.env() == :test,
if @env == :test,
do: [],
else:
[worker(Pleroma.Web.Streamer, [])] ++


+ 42
- 0
lib/pleroma/config.ex 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
- 0
lib/pleroma/emoji.ex 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

+ 28
- 0
lib/pleroma/filter.ex View File

@@ -36,6 +36,34 @@ defmodule Pleroma.Filter do
Repo.all(query)
end

def create(%Pleroma.Filter{user_id: user_id, filter_id: nil} = filter) do
# If filter_id wasn't given, use the max filter_id for this user plus 1.
# XXX This could result in a race condition if a user tries to add two
# different filters for their account from two different clients at the
# same time, but that should be unlikely.

max_id_query =
from(
f in Pleroma.Filter,
where: f.user_id == ^user_id,
select: max(f.filter_id)
)

filter_id =
case Repo.one(max_id_query) do
# Start allocating from 1
nil ->
1

max_id ->
max_id + 1
end

filter
|> Map.put(:filter_id, filter_id)
|> Repo.insert()
end

def create(%Pleroma.Filter{} = filter) do
Repo.insert(filter)
end


+ 20
- 150
lib/pleroma/formatter.ex View File

@@ -2,6 +2,7 @@ defmodule Pleroma.Formatter do
alias Pleroma.User
alias Pleroma.Web.MediaProxy
alias Pleroma.HTML
alias Pleroma.Emoji

@tag_regex ~r/\#\w+/u
def parse_tags(text, data \\ %{}) do
@@ -28,119 +29,10 @@ defmodule Pleroma.Formatter do
|> Enum.filter(fn {_match, user} -> user end)
end

@finmoji [
"a_trusted_friend",
"alandislands",
"association",
"auroraborealis",
"baby_in_a_box",
"bear",
"black_gold",
"christmasparty",
"crosscountryskiing",
"cupofcoffee",
"education",
"fashionista_finns",
"finnishlove",
"flag",
"forest",
"four_seasons_of_bbq",
"girlpower",
"handshake",
"happiness",
"headbanger",
"icebreaker",
"iceman",
"joulutorttu",
"kaamos",
"kalsarikannit_f",
"kalsarikannit_m",
"karjalanpiirakka",
"kicksled",
"kokko",
"lavatanssit",
"losthopes_f",
"losthopes_m",
"mattinykanen",
"meanwhileinfinland",
"moominmamma",
"nordicfamily",
"out_of_office",
"peacemaker",
"perkele",
"pesapallo",
"polarbear",
"pusa_hispida_saimensis",
"reindeer",
"sami",
"sauna_f",
"sauna_m",
"sauna_whisk",
"sisu",
"stuck",
"suomimainittu",
"superfood",
"swan",
"the_cap",
"the_conductor",
"the_king",
"the_voice",
"theoriginalsanta",
"tomoffinland",
"torillatavataan",
"unbreakable",
"waiting",
"white_nights",
"woollysocks"
]

@finmoji_with_filenames Enum.map(@finmoji, fn finmoji ->
{finmoji, "/finmoji/128px/#{finmoji}-128.png"}
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) do
emojify(text, Emoji.get_all())
end

def emojify(text, emoji \\ @emoji)
def emojify(text, nil), do: text

def emojify(text, emoji) do
@@ -160,39 +52,22 @@ defmodule Pleroma.Formatter do
end

def get_emoji(text) when is_binary(text) do
Enum.filter(@emoji, fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
end

def get_emoji(_), do: []

def get_custom_emoji() do
@emoji
end

@link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui

# IANA got a list https://www.iana.org/assignments/uri-schemes/ but
# Stuff like ipfs isn’t in it
# There is very niche stuff
@uri_schemes [
"https://",
"http://",
"dat://",
"dweb://",
"gopher://",
"ipfs://",
"ipns://",
"irc:",
"ircs:",
"magnet:",
"mailto:",
"mumble:",
"ssb://",
"xmpp:"
]
@uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])

# TODO: make it use something other than @link_regex
def html_escape(text) do
def html_escape(text, "text/html") do
HTML.filter_tags(text)
end

def html_escape(text, "text/plain") do
Regex.split(@link_regex, text, include_captures: true)
|> Enum.map_every(2, fn chunk ->
{:safe, part} = Phoenix.HTML.html_escape(chunk)
@@ -203,14 +78,10 @@ defmodule Pleroma.Formatter do

@doc "changes scheme:... urls to html links"
def add_links({subs, text}) do
additionnal_schemes =
Application.get_env(:pleroma, :uri_schemes, [])
|> Keyword.get(:additionnal_schemes, [])

links =
text
|> String.split([" ", "\t", "<br>"])
|> Enum.filter(fn word -> String.starts_with?(word, @uri_schemes ++ additionnal_schemes) end)
|> Enum.filter(fn word -> String.starts_with?(word, @valid_schemes) end)
|> Enum.filter(fn word -> Regex.match?(@link_regex, word) end)
|> Enum.map(fn url -> {Ecto.UUID.generate(), url} end)
|> Enum.sort_by(fn {_, url} -> -String.length(url) end)
@@ -222,13 +93,7 @@ defmodule Pleroma.Formatter do
subs =
subs ++
Enum.map(links, fn {uuid, url} ->
{:safe, link} = Phoenix.HTML.Link.link(url, to: url)

link =
link
|> IO.iodata_to_binary()

{uuid, link}
{uuid, "<a href=\"#{url}\">#{url}</a>"}
end)

{subs, uuid_text}
@@ -250,7 +115,12 @@ defmodule Pleroma.Formatter do
subs =
subs ++
Enum.map(mentions, fn {match, %User{ap_id: ap_id, info: info}, uuid} ->
ap_id = info["source_data"]["url"] || ap_id
ap_id =
if is_binary(info["source_data"]["url"]) do
info["source_data"]["url"]
else
ap_id
end

short_match = String.split(match, "@") |> tl() |> hd()



+ 6
- 9
lib/pleroma/gopher/server.ex View File

@@ -1,16 +1,16 @@
defmodule Pleroma.Gopher.Server do
use GenServer
require Logger
@gopher Application.get_env(:pleroma, :gopher)

def start_link() do
ip = Keyword.get(@gopher, :ip, {0, 0, 0, 0})
port = Keyword.get(@gopher, :port, 1234)
config = Pleroma.Config.get(:gopher, [])
ip = Keyword.get(config, :ip, {0, 0, 0, 0})
port = Keyword.get(config, :port, 1234)
GenServer.start_link(__MODULE__, [ip, port], [])
end

def init([ip, port]) do
if Keyword.get(@gopher, :enabled, false) do
if Pleroma.Config.get([:gopher, :enabled], false) do
Logger.info("Starting gopher server on #{port}")

:ranch.start_listener(
@@ -37,9 +37,6 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
alias Pleroma.Repo
alias Pleroma.HTML

@instance Application.get_env(:pleroma, :instance)
@gopher Application.get_env(:pleroma, :gopher)

def start_link(ref, socket, transport, opts) do
pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts])
{:ok, pid}
@@ -62,7 +59,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do

def link(name, selector, type \\ 1) do
address = Pleroma.Web.Endpoint.host()
port = Keyword.get(@gopher, :port, 1234)
port = Pleroma.Config.get([:gopher, :port], 1234)
"#{type}#{name}\t#{selector}\t#{address}\t#{port}\r\n"
end

@@ -85,7 +82,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
end

def response("") do
info("Welcome to #{Keyword.get(@instance, :name, "Pleroma")}!") <>
info("Welcome to #{Pleroma.Config.get([:instance, :name], "Pleroma")}!") <>
link("Public Timeline", "/main/public") <>
link("Federated Timeline", "/main/all") <> ".\r\n"
end


+ 16
- 10
lib/pleroma/html.ex View File

@@ -1,14 +1,12 @@
defmodule Pleroma.HTML do
alias HtmlSanitizeEx.Scrubber

@markup Application.get_env(:pleroma, :markup)

defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber]
defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers
defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default]

def get_scrubbers() do
Keyword.get(@markup, :scrub_policy)
Pleroma.Config.get([:markup, :scrub_policy])
|> get_scrubbers
end

@@ -36,11 +34,13 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
paragraphs, breaks and links are allowed through the filter.
"""

@markup Application.get_env(:pleroma, :markup)
@uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])

require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta

@valid_schemes ["http", "https"]

Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()

@@ -56,11 +56,11 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.allow_tag_with_these_attributes("span", [])

# allow inline images for custom emoji
@markup Application.get_env(:pleroma, :markup)
@allow_inline_images Keyword.get(@markup, :allow_inline_images)

if @allow_inline_images do
Meta.allow_tag_with_uri_attributes("img", ["src"], @valid_schemes)
# restrict img tags to http/https only, because of MediaProxy.
Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"])

Meta.allow_tag_with_these_attributes("img", [
"width",
@@ -79,7 +79,9 @@ defmodule Pleroma.HTML.Scrubber.Default do
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta

@valid_schemes ["http", "https"]
@markup Application.get_env(:pleroma, :markup)
@uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])

Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
@@ -87,6 +89,8 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title"])

Meta.allow_tag_with_these_attributes("abbr", ["title"])

Meta.allow_tag_with_these_attributes("b", [])
Meta.allow_tag_with_these_attributes("blockquote", [])
Meta.allow_tag_with_these_attributes("br", [])
@@ -103,11 +107,11 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("u", [])
Meta.allow_tag_with_these_attributes("ul", [])

@markup Application.get_env(:pleroma, :markup)
@allow_inline_images Keyword.get(@markup, :allow_inline_images)

if @allow_inline_images do
Meta.allow_tag_with_uri_attributes("img", ["src"], @valid_schemes)
# restrict img tags to http/https only, because of MediaProxy.
Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"])

Meta.allow_tag_with_these_attributes("img", [
"width",
@@ -173,6 +177,8 @@ defmodule Pleroma.HTML.Transform.MediaProxy do
{"img", attributes, children}
end

def scrub({:comment, children}), do: ""

def scrub({tag, attributes, children}), do: {tag, attributes, children}
def scrub({tag, children}), do: children
def scrub(text), do: text


+ 1
- 0
lib/pleroma/http/http.ex View File

@@ -22,6 +22,7 @@ defmodule Pleroma.HTTP do
def process_request_options(options) do
config = Application.get_env(:pleroma, :http, [])
proxy = Keyword.get(config, :proxy_url, nil)
options = options ++ [hackney: [pool: :default]]

case proxy do
nil -> options


+ 19
- 0
lib/pleroma/list.ex View File

@@ -69,6 +69,25 @@ defmodule Pleroma.List do
Repo.all(query)
end

# Get lists to which the account belongs.
def get_lists_account_belongs(%User{} = owner, account_id) do
user = Repo.get(User, account_id)

query =
from(
l in Pleroma.List,
where:
l.user_id == ^owner.id and
fragment(
"? = ANY(?)",
^user.follower_address,
l.following
)
)

Repo.all(query)
end

def rename(%Pleroma.List{} = list, title) do
list
|> title_changeset(%{title: title})


+ 76
- 2
lib/pleroma/notification.ex View File

@@ -1,6 +1,6 @@
defmodule Pleroma.Notification do
use Ecto.Schema
alias Pleroma.{User, Activity, Notification, Repo}
alias Pleroma.{User, Activity, Notification, Repo, Object}
import Ecto.Query

schema "notifications" do
@@ -42,6 +42,20 @@ defmodule Pleroma.Notification do
Repo.all(query)
end

def set_read_up_to(%{id: user_id} = _user, id) do
query =
from(
n in Notification,
where: n.user_id == ^user_id,
where: n.id <= ^id,
update: [
set: [seen: true]
]
)

Repo.update_all(query, [])
end

def get(%{id: user_id} = _user, id) do
query =
from(
@@ -81,7 +95,7 @@ defmodule Pleroma.Notification do

def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity)
when type in ["Create", "Like", "Announce", "Follow"] do
users = User.get_notified_from_activity(activity)
users = get_notified_from_activity(activity)

notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
@@ -100,4 +114,64 @@ defmodule Pleroma.Notification do
notification
end
end

def get_notified_from_activity(activity, local_only \\ true)

def get_notified_from_activity(
%Activity{data: %{"to" => _, "type" => type} = data} = activity,
local_only
)
when type in ["Create", "Like", "Announce", "Follow"] do
recipients =
[]
|> maybe_notify_to_recipients(activity)
|> maybe_notify_mentioned_recipients(activity)
|> Enum.uniq()

User.get_users_from_set(recipients, local_only)
end

def get_notified_from_activity(_, local_only), do: []

defp maybe_notify_to_recipients(
recipients,
%Activity{data: %{"to" => to, "type" => type}} = activity
) do
recipients ++ to
end

defp maybe_notify_mentioned_recipients(
recipients,
%Activity{data: %{"to" => to, "type" => type} = data} = activity
)
when type == "Create" do
object = Object.normalize(data["object"])

object_data =
cond do
!is_nil(object) ->
object.data

is_map(data["object"]) ->
data["object"]

true ->
%{}
end

tagged_mentions = maybe_extract_mentions(object_data)

recipients ++ tagged_mentions
end

defp maybe_notify_mentioned_recipients(recipients, _), do: recipients

defp maybe_extract_mentions(%{"tag" => tag}) do
tag
|> Enum.filter(fn x -> is_map(x) end)
|> Enum.filter(fn x -> x["type"] == "Mention" end)
|> Enum.map(fn x -> x["href"] end)
end

defp maybe_extract_mentions(_), do: []
end

+ 15
- 5
lib/pleroma/object.ex View File

@@ -1,6 +1,6 @@
defmodule Pleroma.Object do
use Ecto.Schema
alias Pleroma.{Repo, Object}
alias Pleroma.{Repo, Object, Activity}
import Ecto.{Query, Changeset}

schema "objects" do
@@ -31,13 +31,15 @@ defmodule Pleroma.Object do
def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
def normalize(_), do: nil

def get_cached_by_ap_id(ap_id) do
if Mix.env() == :test do
if Mix.env() == :test do
def get_cached_by_ap_id(ap_id) do
get_by_ap_id(ap_id)
else
end
else
def get_cached_by_ap_id(ap_id) do
key = "object:#{ap_id}"

Cachex.fetch!(:user_cache, key, fn _ ->
Cachex.fetch!(:object_cache, key, fn _ ->
object = get_by_ap_id(ap_id)

if object do
@@ -52,4 +54,12 @@ defmodule Pleroma.Object do
def context_mapping(context) do
Object.change(%Object{}, %{data: %{"id" => context}})
end

def delete(%Object{data: %{"id" => id}} = object) do
with Repo.delete(object),
Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
{:ok, object}
end
end
end

+ 18
- 0
lib/pleroma/plugs/federating_plug.ex 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

+ 63
- 0
lib/pleroma/plugs/http_security_plug.ex View File

@@ -0,0 +1,63 @@
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
protocol = Config.get([Pleroma.Web.Endpoint, :protocol])

[
"default-src 'none'",
"base-uri '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' " <> String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
"manifest-src 'self'",
if @protocol == "https" do
"upgrade-insecure-requests"
end
]
|> 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

+ 19
- 0
lib/pleroma/plugs/user_is_admin_plug.ex View File

@@ -0,0 +1,19 @@
defmodule Pleroma.Plugs.UserIsAdminPlug do
import Plug.Conn
alias Pleroma.User

def init(options) do
options
end

def call(%{assigns: %{user: %User{info: %{"is_admin" => true}}}} = conn, _) do
conn
end

def call(conn, _) do
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{error: "User is not admin."}))
|> halt
end
end

+ 71
- 51
lib/pleroma/upload.ex View File

@@ -1,64 +1,74 @@
defmodule Pleroma.Upload do
alias Ecto.UUID

@storage_backend Application.get_env(:pleroma, Pleroma.Upload)
|> Keyword.fetch!(:uploader)
def check_file_size(path, nil), do: true

def store(%Plug.Upload{} = file, should_dedupe) do
def check_file_size(path, size_limit) do
{:ok, %{size: size}} = File.stat(path)
size <= size_limit
end

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)

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
}
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) do
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)
data = Base.decode64!(parsed["data"], ignore: :whitespace)

tmp_path = tempfile_for_image(data)

uuid = UUID.generate()

content_type = get_content_type(tmp_path)
strip_exif_data(content_type, tmp_path)

name =
create_name(
String.downcase(Base.encode16(:crypto.hash(:sha256, data))),
parsed["filetype"],
content_type
)

{:ok, url_path} = @storage_backend.put_file(name, uuid, tmp_path, content_type, should_dedupe)

%{
"type" => "Image",
"url" => [
%{
"type" => "Link",
"mediaType" => content_type,
"href" => url_path
}
],
"name" => name
}
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)

name =
create_name(
String.downcase(Base.encode16(:crypto.hash(:sha256, data))),
parsed["filetype"],
content_type
)

{:ok, url_path} = uploader().put_file(name, uuid, tmp_path, content_type, should_dedupe)

%{
"type" => "Image",
"url" => [
%{
"type" => "Link",
"mediaType" => content_type,
"href" => url_path
}
],
"name" => name
}
else
_e -> nil
end
end

@doc """
@@ -152,7 +162,13 @@ defmodule Pleroma.Upload do
"audio/mpeg"

<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> ->
"audio/ogg"
case IO.binread(f, 27) do
<<_::size(160), 0x80, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61>> ->
"video/ogg"

_ ->
"audio/ogg"
end

<<0x52, 0x49, 0x46, 0x46, _, _, _, _>> ->
"audio/wav"
@@ -167,4 +183,8 @@ defmodule Pleroma.Upload do
_e -> "application/octet-stream"
end
end

defp uploader() do
Pleroma.Config.get!([Pleroma.Upload, :uploader])
end
end

+ 26
- 0
lib/pleroma/uploaders/mdii.ex View File

@@ -0,0 +1,26 @@
defmodule Pleroma.Uploaders.MDII do
alias Pleroma.Config

@behaviour Pleroma.Uploaders.Uploader

@httpoison Application.get_env(:pleroma, :httpoison)

def put_file(name, uuid, path, content_type, should_dedupe) do
cgi = Pleroma.Config.get([Pleroma.Uploaders.MDII, :cgi])
files = Pleroma.Config.get([Pleroma.Uploaders.MDII, :files])

{:ok, file_data} = File.read(path)

extension = String.split(name, ".") |> List.last()
query = "#{cgi}?#{extension}"

with {:ok, %{status_code: 200, body: body}} <- @httpoison.post(query, file_data) do
File.rm!(path)
remote_file_name = String.split(body) |> List.first()
public_url = "#{files}/#{remote_file_name}.#{extension}"
{:ok, public_url}
else
_ -> Pleroma.Uploaders.Local.put_file(name, uuid, path, content_type, should_dedupe)
end
end
end

+ 18
- 2
lib/pleroma/uploaders/s3.ex View File

@@ -1,16 +1,19 @@
defmodule Pleroma.Uploaders.S3 do
alias Pleroma.Web.MediaProxy

@behaviour Pleroma.Uploaders.Uploader

def put_file(name, uuid, path, content_type, _should_dedupe) do
settings = Application.get_env(:pleroma, Pleroma.Uploaders.S3)
bucket = Keyword.fetch!(settings, :bucket)
public_endpoint = Keyword.fetch!(settings, :public_endpoint)
force_media_proxy = Keyword.fetch!(settings, :force_media_proxy)

{:ok, file_data} = File.read(path)

File.rm!(path)

s3_name = "#{uuid}/#{name}"
s3_name = "#{uuid}/#{encode(name)}"

{:ok, _} =
ExAws.S3.put_object(bucket, s3_name, file_data, [
@@ -19,6 +22,19 @@ defmodule Pleroma.Uploaders.S3 do
])
|> ExAws.request()

{:ok, "#{public_endpoint}/#{bucket}/#{s3_name}"}
url_base = "#{public_endpoint}/#{bucket}/#{s3_name}"

public_url =
if force_media_proxy do
MediaProxy.url(url_base)
else
url_base
end

{:ok, public_url}
end

defp encode(name) do
String.replace(name, ~r/[^0-9a-zA-Z!.*'()_-]/, "-")
end
end

+ 5
- 6
lib/pleroma/uploaders/swift/keystone.ex View File

@@ -1,11 +1,9 @@
defmodule Pleroma.Uploaders.Swift.Keystone do
use HTTPoison.Base

@settings Application.get_env(:pleroma, Pleroma.Uploaders.Swift)

def process_url(url) do
Enum.join(
[Keyword.fetch!(@settings, :auth_url), url],
[Pleroma.Config.get!([Pleroma.Uploaders.Swift, :auth_url]), url],
"/"
)
end
@@ -16,9 +14,10 @@ defmodule Pleroma.Uploaders.Swift.Keystone do
end

def get_token() do
username = Keyword.fetch!(@settings, :username)
password = Keyword.fetch!(@settings, :password)
tenant_id = Keyword.fetch!(@settings, :tenant_id)
settings = Pleroma.Config.get(Pleroma.Uploaders.Swift)
username = Keyword.fetch!(settings, :username)
password = Keyword.fetch!(settings, :password)
tenant_id = Keyword.fetch!(settings, :tenant_id)

case post(
"/tokens",


+ 2
- 4
lib/pleroma/uploaders/swift/swift.ex View File

@@ -1,17 +1,15 @@
defmodule Pleroma.Uploaders.Swift.Client do
use HTTPoison.Base

@settings Application.get_env(:pleroma, Pleroma.Uploaders.Swift)

def process_url(url) do
Enum.join(
[Keyword.fetch!(@settings, :storage_url), url],
[Pleroma.Config.get!([Pleroma.Uploaders.Swift, :storage_url]), url],
"/"
)
end

def upload_file(filename, body, content_type) do
object_url = Keyword.fetch!(@settings, :object_url)
object_url = Pleroma.Config.get!([Pleroma.Uploaders.Swift, :object_url])
token = Pleroma.Uploaders.Swift.Keystone.get_token()

case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do


+ 57
- 52
lib/pleroma/user.ex View File

@@ -4,7 +4,7 @@ defmodule Pleroma.User do
import Ecto.{Changeset, Query}
alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
alias Comeonin.Pbkdf2
alias Pleroma.Web.{OStatus, Websub}
alias Pleroma.Web.{OStatus, Websub, OAuth}
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}

schema "users" do
@@ -42,6 +42,10 @@ defmodule Pleroma.User do
end
end

def profile_url(%User{info: %{"source_data" => %{"url" => url}}}), do: url
def profile_url(%User{ap_id: ap_id}), do: ap_id
def profile_url(_), do: nil

def ap_id(%User{nickname: nickname}) do
"#{Web.base_url()}/users/#{nickname}"
end
@@ -132,6 +136,9 @@ defmodule Pleroma.User do
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)

OAuth.Token.delete_user_tokens(struct)
OAuth.Authorization.delete_user_authorizations(struct)

if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])

@@ -184,33 +191,16 @@ defmodule Pleroma.User do

def needs_update?(_), do: true

def maybe_direct_follow(%User{} = follower, %User{info: info} = followed) do
user_config = Application.get_env(:pleroma, :user)
deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)

user_info = user_info(followed)

should_direct_follow =
cond do
# if the account is locked, don't pre-create the relationship
user_info[:locked] == true ->
false

# if the users are blocking each other, we shouldn't even be here, but check for it anyway
deny_follow_blocked and
(User.blocks?(follower, followed) or User.blocks?(followed, follower)) ->
false

# if OStatus, then there is no three-way handshake to follow
User.ap_enabled?(followed) != true ->
true
def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{"locked" => true}}) do
{:ok, follower}
end

# if there are no other reasons not to, just pre-create the relationship
true ->
true
end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
follow(follower, followed)
end

if should_direct_follow do
def maybe_direct_follow(%User{} = follower, %User{} = followed) do
if !User.ap_enabled?(followed) do
follow(follower, followed)
else
{:ok, follower}
@@ -305,6 +295,7 @@ defmodule Pleroma.User do
def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}")
Cachex.del(:user_cache, "user_info:#{user.id}")
end

def get_cached_by_ap_id(ap_id) do
@@ -473,36 +464,25 @@ defmodule Pleroma.User do
update_and_set_cache(cs)
end

def get_notified_from_activity_query(to) do
def get_users_from_set_query(ap_ids, false) do
from(
u in User,
where: u.ap_id in ^to,
where: u.local == true
where: u.ap_id in ^ap_ids
)
end

def get_notified_from_activity(%Activity{recipients: to, data: %{"type" => "Announce"} = data}) do
object = Object.normalize(data["object"])
actor = User.get_cached_by_ap_id(data["actor"])

# ensure that the actor who published the announced object appears only once
to =
if actor.nickname != nil do
to ++ [object.data["actor"]]
else
to
end
|> Enum.uniq()

query = get_notified_from_activity_query(to)
def get_users_from_set_query(ap_ids, true) do
query = get_users_from_set_query(ap_ids, false)

Repo.all(query)
from(
u in query,
where: u.local == true
)
end

def get_notified_from_activity(%Activity{recipients: to}) do
query = get_notified_from_activity_query(to)

Repo.all(query)
def get_users_from_set(ap_ids, local_only \\ true) do
get_users_from_set_query(ap_ids, local_only)
|> Repo.all()
end

def get_recipients_from_activity(%Activity{recipients: to}) do
@@ -518,7 +498,7 @@ defmodule Pleroma.User do
Repo.all(query)
end

def search(query, resolve) do
def search(query, resolve \\ false) do
# strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")

@@ -632,8 +612,8 @@ defmodule Pleroma.User do
)
end

def deactivate(%User{} = user) do
new_info = Map.put(user.info, "deactivated", true)
def deactivate(%User{} = user, status \\ true) do
new_info = Map.put(user.info, "deactivated", status)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
@@ -666,7 +646,7 @@ defmodule Pleroma.User do
end
end)

:ok
{:ok, user}
end

def html_filter_policy(%User{info: %{"no_rich_text" => true}}) do
@@ -753,6 +733,7 @@ defmodule Pleroma.User do
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
end

def ap_enabled?(%User{local: true}), do: true
def ap_enabled?(%User{info: info}), do: info["ap_enabled"]
def ap_enabled?(_), do: false

@@ -763,4 +744,28 @@ defmodule Pleroma.User do
get_or_fetch_by_nickname(uri_or_nickname)
end
end

# wait a period of time and return newest version of the User structs
# this is because we have synchronous follow APIs and need to simulate them
# with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
with %User{} = a <- Repo.get(User, a.id),
%User{} = b <- Repo.get(User, b.id) do
{:ok, a, b}
else
_e ->
:error
end
end

def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout),
%User{} = a <- Repo.get(User, a.id),
%User{} = b <- Repo.get(User, b.id) do
{:ok, a, b}
else
_e ->
:error
end
end
end

+ 71
- 26
lib/pleroma/web/activity_pub/activity_pub.ex View File

@@ -10,8 +10,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

@httpoison Application.get_env(:pleroma, :httpoison)

@instance Application.get_env(:pleroma, :instance)

# For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do
@@ -44,7 +42,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp check_actor_is_active(actor) do
if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor),
nil <- user.info["deactivated"] do
false <- !!user.info["deactivated"] do
:ok
else
_e -> :reject
@@ -273,8 +271,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"]
}

with Repo.delete(object),
Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
with {:ok, _} <- Object.delete(object),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity),
{:ok, _actor} <- User.decrease_note_count(user) do
@@ -575,9 +572,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.reverse()
end

def upload(file) do
data = Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media])
Repo.insert(%Object{data: data})
def upload(file, size_limit \\ nil) do
with data <-
Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media], size_limit),
false <- is_nil(data) do
Repo.insert(%Object{data: data})
end
end

def user_data_from_user_object(data) do
@@ -628,9 +628,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end

def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, %{status_code: 200, body: body}} <-
@httpoison.get(ap_id, [Accept: "application/activity+json"], follow_redirect: true),
{:ok, data} <- Jason.decode(body) do
with {:ok, data} <- fetch_and_contain_remote_object_from_id(ap_id) do
user_data_from_user_object(data)
else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
@@ -657,14 +655,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end

@quarantined_instances Keyword.get(@instance, :quarantined_instances, [])

def should_federate?(inbox, public) do
if public do
true
else
inbox_info = URI.parse(inbox)
inbox_info.host not in @quarantined_instances
!Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
end

@@ -683,7 +679,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
(Pleroma.Web.Salmon.remote_users(activity) ++ followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{"source_data" => data}} ->
(data["endpoints"] && data["endpoints"]["sharedInbox"]) || data["inbox"]
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
@@ -734,28 +730,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
else
Logger.info("Fetching #{id} via AP")

with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
@httpoison.get(
id,
[Accept: "application/activity+json"],
follow_redirect: true,
timeout: 10000,
recv_timeout: 20000
),
{:ok, data} <- Jason.decode(body),
with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
nil <- Object.normalize(data),
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["attributedTo"],
"actor" => data["actor"] || data["attributedTo"],
"object" => data
},
:ok <- Transmogrifier.contain_origin(id, params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.normalize(activity.data["object"])}
else
{:error, {:reject, nil}} ->
{:reject, nil}

object = %Object{} ->
{:ok, object}

@@ -770,6 +760,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end

def fetch_and_contain_remote_object_from_id(id) do
Logger.info("Fetching #{id} via AP")

with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
@httpoison.get(
id,
[Accept: "application/activity+json"],
follow_redirect: true,
timeout: 10000,
recv_timeout: 20000
),
{:ok, data} <- Jason.decode(body),
:ok <- Transmogrifier.contain_origin_from_id(id, data) do
{:ok, data}
else
e ->
{:error, e}
end
end

def is_public?(activity) do
"https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
(activity.data["cc"] || []))
@@ -784,4 +795,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
y = activity.data["to"] ++ (activity.data["cc"] || [])
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end

# guard
def entire_thread_visible_for_user?(nil, user), do: false

# child
def entire_thread_visible_for_user?(
%Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
user
)
when is_binary(parent_id) do
parent = Activity.get_in_reply_to_activity(tail)
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
end

# root
def entire_thread_visible_for_user?(tail, user), do: visible_for_user?(tail, user)

# filter out broken threads
def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user)
end

# do post-processing on a specific activity
def contain_activity(%Activity{} = activity, %User{} = user) do
contain_broken_threads(activity, user)
end

# do post-processing on a timeline
def contain_timeline(timeline, user) do
timeline
|> Enum.filter(fn activity ->
contain_activity(activity, user)
end)
end
end

+ 41
- 8
lib/pleroma/web/activity_pub/activity_pub_controller.ex View File

@@ -4,12 +4,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Web.ActivityPub.{ObjectView, UserView}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator

require Logger

action_fallback(:errors)

plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
plug(:relay_active? when action in [:relay])

def relay_active?(conn, _) do
if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do
conn
else
conn
|> put_status(404)
|> json(%{error: "not found"})
|> halt
end
end

def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
@@ -87,25 +102,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
outbox(conn, %{"nickname" => nickname, "max_id" => nil})
end

# TODO: Ensure that this inbox is a recipient of the message
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
true <- Utils.recipient_in_message(user.ap_id, params),
params <- Utils.maybe_splice_recipient(user.ap_id, params) do
Federator.enqueue(:incoming_ap_doc, params)
json(conn, "ok")
end
end

def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
Federator.enqueue(:incoming_ap_doc, params)
json(conn, "ok")
end

# only accept relayed Creates
def inbox(conn, %{"type" => "Create"} = params) do
Logger.info(
"Signature missing or not from author, relayed Create message, fetching object from source"
)

ActivityPub.fetch_object_from_id(params["object"]["id"])

json(conn, "ok")
end

def inbox(conn, params) do
headers = Enum.into(conn.req_headers, %{})

if !String.contains?(headers["signature"] || "", params["actor"]) do
Logger.info("Signature not from author, relayed message, fetching from source")
ActivityPub.fetch_object_from_id(params["object"]["id"])
else
Logger.info("Signature error - make sure you are forwarding the HTTP Host header!")
Logger.info("Could not validate #{params["actor"]}")
if String.contains?(headers["signature"], params["actor"]) do
Logger.info(
"Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
)

Logger.info(inspect(conn.req_headers))
end

json(conn, "ok")
json(conn, "error")
end

def relay(conn, params) do


+ 1
- 3
lib/pleroma/web/activity_pub/mrf/normalize_markup.ex View File

@@ -3,10 +3,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do

@behaviour Pleroma.Web.ActivityPub.MRF

@mrf_normalize_markup Application.get_env(:pleroma, :mrf_normalize_markup)

def filter(%{"type" => activity_type} = object) when activity_type == "Create" do
scrub_policy = Keyword.get(@mrf_normalize_markup, :scrub_policy)
scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])

child = object["object"]



+ 4
- 6
lib/pleroma/web/activity_pub/mrf/reject_non_public.ex View File

@@ -2,10 +2,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF

@mrf_rejectnonpublic Application.get_env(:pleroma, :mrf_rejectnonpublic)
@allow_followersonly Keyword.get(@mrf_rejectnonpublic, :allow_followersonly)
@allow_direct Keyword.get(@mrf_rejectnonpublic, :allow_direct)

@impl true
def filter(%{"type" => "Create"} = object) do
user = User.get_cached_by_ap_id(object["actor"])
@@ -20,6 +16,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
true -> "direct"
end

policy = Pleroma.Config.get(:mrf_rejectnonpublic)

case visibility do
"public" ->
{:ok, object}
@@ -28,14 +26,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
{:ok, object}

"followers" ->
with true <- @allow_followersonly do
with true <- Keyword.get(policy, :allow_followersonly) do
{:ok, object}
else
_e -> {:reject, nil}
end

"direct" ->
with true <- @allow_direct do
with true <- Keyword.get(policy, :allow_direct) do
{:ok, object}
else
_e -> {:reject, nil}


+ 51
- 37
lib/pleroma/web/activity_pub/mrf/simple_policy.ex View File

@@ -2,60 +2,76 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF

@mrf_policy Application.get_env(:pleroma, :mrf_simple)

@accept Keyword.get(@mrf_policy, :accept)
defp check_accept(%{host: actor_host} = actor_info, object)
when length(@accept) > 0 and not (actor_host in @accept) do
{:reject, nil}
defp check_accept(%{host: actor_host} = _actor_info, object) do
accepts = Pleroma.Config.get([:mrf_simple, :accept])

cond do
accepts == [] -> {:ok, object}
actor_host == Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
Enum.member?(accepts, actor_host) -> {:ok, object}
true -> {:reject, nil}
end
end

defp check_accept(actor_info, object), do: {:ok, object}

@reject Keyword.get(@mrf_policy, :reject)
defp check_reject(%{host: actor_host} = actor_info, object) when actor_host in @reject do
{:reject, nil}
defp check_reject(%{host: actor_host} = _actor_info, object) do
if Enum.member?(Pleroma.Config.get([:mrf_simple, :reject]), actor_host) do
{:reject, nil}
else
{:ok, object}
end
end

defp check_reject(actor_info, object), do: {:ok, object}
defp check_media_removal(
%{host: actor_host} = _actor_info,
%{"type" => "Create", "object" => %{"attachement" => child_attachment}} = object
)
when length(child_attachment) > 0 do
object =
if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_removal]), actor_host) do
child_object = Map.delete(object["object"], "attachment")
Map.put(object, "object", child_object)
else
object
end

@media_removal Keyword.get(@mrf_policy, :media_removal)
defp check_media_removal(%{host: actor_host} = actor_info, %{"type" => "Create"} = object)
when actor_host in @media_removal do
child_object = Map.delete(object["object"], "attachment")
object = Map.put(object, "object", child_object)
{:ok, object}
end

defp check_media_removal(actor_info, object), do: {:ok, object}
defp check_media_removal(_actor_info, object), do: {:ok, object}

@media_nsfw Keyword.get(@mrf_policy, :media_nsfw)
defp check_media_nsfw(
%{host: actor_host} = actor_info,
%{host: actor_host} = _actor_info,
%{
"type" => "Create",
"object" => %{"attachment" => child_attachment} = child_object
} = object
)
when actor_host in @media_nsfw and length(child_attachment) > 0 do
tags = (child_object["tag"] || []) ++ ["nsfw"]
child_object = Map.put(child_object, "tags", tags)
child_object = Map.put(child_object, "sensitive", true)
object = Map.put(object, "object", child_object)
when length(child_attachment) > 0 do
object =
if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_nsfw]), actor_host) do
tags = (child_object["tag"] || []) ++ ["nsfw"]
child_object = Map.put(child_object, "tags", tags)
child_object = Map.put(child_object, "sensitive", true)
Map.put(object, "object", child_object)
else
object
end

{:ok, object}
end

defp check_media_nsfw(actor_info, object), do: {:ok, object}

@ftl_removal Keyword.get(@mrf_policy, :federated_timeline_removal)
defp check_ftl_removal(%{host: actor_host} = actor_info, object)
when actor_host in @ftl_removal do
user = User.get_by_ap_id(object["actor"])
defp check_media_nsfw(_actor_info, object), do: {:ok, object}

# flip to/cc relationship to make the post unlisted
defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
object =
if "https://www.w3.org/ns/activitystreams#Public" in object["to"] and
user.follower_address in object["cc"] do
with true <-
Enum.member?(
Pleroma.Config.get([:mrf_simple, :federated_timeline_removal]),
actor_host
),
user <- User.get_cached_by_ap_id(object["actor"]),
true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"],
true <- user.follower_address in object["cc"] do
to =
List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++
[user.follower_address]
@@ -68,14 +84,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|> Map.put("to", to)
|> Map.put("cc", cc)
else
object
_ -> object
end

{:ok, object}
end

defp check_ftl_removal(actor_info, object), do: {:ok, object}

@impl true
def filter(object) do
actor_info = URI.parse(object["actor"])


+ 23
- 0
lib/pleroma/web/activity_pub/mrf/user_allowlist.ex View File

@@ -0,0 +1,23 @@
defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
alias Pleroma.Config

@behaviour Pleroma.Web.ActivityPub.MRF

defp filter_by_list(object, []), do: {:ok, object}

defp filter_by_list(%{"actor" => actor} = object, allow_list) do
if actor in allow_list do
{:ok, object}
else
{:reject, nil}
end
end

@impl true
def filter(object) do
actor_info = URI.parse(object["actor"])
allow_list = Config.get([:mrf_user_allowlist, String.to_atom(actor_info.host)], [])

filter_by_list(object, allow_list)
end
end

+ 8
- 6
lib/pleroma/web/activity_pub/relay.ex View File

@@ -12,11 +12,12 @@ defmodule Pleroma.Web.ActivityPub.Relay do
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.follow(local_user, target_user) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
else
e -> Logger.error("error: #{inspect(e)}")
e ->
Logger.error("error: #{inspect(e)}")
{:error, e}
end

:ok
end

def unfollow(target_instance) do
@@ -24,11 +25,12 @@ defmodule Pleroma.Web.ActivityPub.Relay do
%User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}
else
e -> Logger.error("error: #{inspect(e)}")
e ->
Logger.error("error: #{inspect(e)}")
{:error, e}
end

:ok
end

def publish(%Activity{data: %{"type" => "Create"}} = activity) do


+ 168
- 62
lib/pleroma/web/activity_pub/transmogrifier.ex View File

@@ -21,18 +21,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
if is_binary(Enum.at(actor, 0)) do
Enum.at(actor, 0)
else
Enum.find(actor, fn %{"type" => type} -> type == "Person" end)
Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
|> Map.get("id")
end
end

def get_actor(%{"actor" => actor}) when is_map(actor) do
actor["id"]
def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
id
end

def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
get_actor(%{"actor" => actor})
end

@doc """
Checks that an imported AP object's actor matches the domain it came from.
"""
def contain_origin(id, %{"actor" => nil}), do: :error

def contain_origin(id, %{"actor" => actor} = params) do
id_uri = URI.parse(id)
actor_uri = URI.parse(get_actor(params))
@@ -44,6 +50,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end

def contain_origin_from_id(id, %{"id" => nil}), do: :error

def contain_origin_from_id(id, %{"id" => other_id} = params) do
id_uri = URI.parse(id)
other_uri = URI.parse(other_id)

if id_uri.host == other_uri.host do
:ok
else
:error
end
end

@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
@@ -51,6 +70,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
|> fix_actor
|> fix_attachments
|> fix_url
|> fix_context
|> fix_in_reply_to
|> fix_emoji
@@ -96,9 +116,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
end

def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object)
when not is_nil(in_reply_to_id) do
case ActivityPub.fetch_object_from_id(in_reply_to_id) do
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
when not is_nil(in_reply_to) do
in_reply_to_id =
cond do
is_bitstring(in_reply_to) ->
in_reply_to

is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]

is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)

# Maybe I should output an error too?
true ->
""
end

case fetch_obj_helper(in_reply_to_id) do
{:ok, replied_object} ->
with %Activity{} = activity <-
Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) do
@@ -110,12 +146,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
e ->
Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
object
end

e ->
Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
object
end
end
@@ -130,9 +166,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("conversation", context)
end

def fix_attachments(object) do
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
(object["attachment"] || [])
attachment
|> Enum.map(fn data ->
url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
Map.put(data, "url", url)
@@ -142,21 +178,41 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("attachment", attachments)
end

def fix_emoji(object) do
tags = object["tag"] || []
def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
Map.put(object, "attachment", [attachment])
|> fix_attachments()
end

def fix_attachments(object), do: object

def fix_url(%{"url" => url} = object) when is_map(url) do
object
|> Map.put("url", url["href"])
end

def fix_url(%{"url" => url} = object) when is_list(url) do
first_element = Enum.at(url, 0)

url_string =
cond do
is_bitstring(first_element) -> first_element
is_map(first_element) -> first_element["href"] || ""
true -> ""
end

object
|> Map.put("url", url_string)
end

def fix_url(object), do: object

def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)

emoji =
emoji
|> Enum.reduce(%{}, fn data, mapping ->
name = data["name"]

name =
if String.starts_with?(name, ":") do
name |> String.slice(1..-2)
else
name
end
name = String.trim(data["name"], ":")

mapping |> Map.put(name, data["icon"]["url"])
end)
@@ -168,18 +224,37 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("emoji", emoji)
end

def fix_tag(object) do
def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
name = String.trim(tag["name"], ":")
emoji = %{name => tag["icon"]["url"]}

object
|> Map.put("emoji", emoji)
end

def fix_emoji(object), do: object

def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
tags =
(object["tag"] || [])
tag
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)

combined = (object["tag"] || []) ++ tags
combined = tag ++ tags

object
|> Map.put("tag", combined)
end

def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
combined = [tag, String.slice(hashtag, 1..-1)]

object
|> Map.put("tag", combined)
end

def fix_tag(object), do: object

# content map usually only has one language so this will do for now.
def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map)
@@ -201,7 +276,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# - tags
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
when objtype in ["Article", "Note", "Video"] do
when objtype in ["Article", "Note", "Video", "Page"] do
actor = get_actor(data)

data =
@@ -285,8 +360,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
) do
with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
with actor <- get_actor(data),
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
@@ -309,8 +386,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
) do
with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
with actor <- get_actor(data),
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
@@ -329,11 +408,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end

def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = _data
%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
with actor <- get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
{:ok, activity}
else
@@ -342,11 +421,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end

def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = _data
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
with actor <- get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
{:ok, activity}
else
@@ -388,15 +467,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end

# TODO: Make secure.
# TODO: We presently assume that any actor on the same origin domain as the object being
# deleted has the rights to delete that object. A better way to validate whether or not
# the object should be deleted is to refetch the object URI, which should return either
# an error or a tombstone. This would allow us to verify that a deletion actually took
# place.
def handle_incoming(
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = _data
%{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
) do
object_id = Utils.get_ap_id(object_id)

with %User{} = _actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
with actor <- get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
:ok <- contain_origin(actor.ap_id, object.data),
{:ok, activity} <- ActivityPub.delete(object, false) do
{:ok, activity}
else
@@ -410,11 +494,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"object" => %{"type" => "Announce", "object" => object_id},
"actor" => actor,
"id" => id
} = _data
} = data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
with actor <- get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity}
else
@@ -440,9 +524,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end

@ap_config Application.get_env(:pleroma, :activitypub)
@accept_blocks Keyword.get(@ap_config, :accept_blocks)

def handle_incoming(
%{
"type" => "Undo",
@@ -451,7 +532,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"id" => id
} = _data
) do
with true <- @accept_blocks,
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
%User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
@@ -465,7 +546,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
) do
with true <- @accept_blocks,
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
%User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
@@ -483,11 +564,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
"object" => %{"type" => "Like", "object" => object_id},
"actor" => actor,
"id" => id
} = _data
} = data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
with actor <- get_actor(data),
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity}
else
@@ -497,6 +578,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do

def handle_incoming(_), do: :error

def fetch_obj_helper(id) when is_bitstring(id), do: ActivityPub.fetch_object_from_id(id)
def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_id(obj["id"])

def get_obj_helper(id) do
if object = Object.normalize(id), do: {:ok, object}, else: nil
end
@@ -523,6 +607,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
|> strip_internal_fields
|> strip_internal_tags
end

# @doc
@@ -538,7 +624,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data =
data
|> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
|> Map.merge(Utils.make_json_ld_header())

{:ok, data}
end
@@ -557,7 +643,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data =
data
|> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
|> Map.merge(Utils.make_json_ld_header())

{:ok, data}
end
@@ -575,7 +661,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data =
data
|> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
|> Map.merge(Utils.make_json_ld_header())

{:ok, data}
end
@@ -585,14 +671,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data =
data
|> maybe_fix_object_url
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
|> Map.merge(Utils.make_json_ld_header())

{:ok, data}
end

def maybe_fix_object_url(data) do
if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
case ActivityPub.fetch_object_from_id(data["object"]) do
case fetch_obj_helper(data["object"]) do
{:ok, relative_object} ->
if relative_object.data["external_url"] do
_data =
@@ -627,12 +713,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end

def add_mention_tags(object) do
recipients = object["to"] ++ (object["cc"] || [])

mentions =
recipients
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.filter(& &1)
object
|> Utils.get_notified_from_object()
|> Enum.map(fn user ->
%{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
end)
@@ -692,6 +775,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("attachment", attachments)
end

defp strip_internal_fields(object) do
object
|> Map.drop([
"likes",
"like_count",
"announcements",
"announcement_count",
"emoji",
"context_id"
])
end

defp strip_internal_tags(%{"tag" => tags} = object) do
tags =
tags
|> Enum.filter(fn x -> is_map(x) end)

object
|> Map.put("tag", tags)
end

defp strip_internal_tags(object), do: object

defp user_upgrade_task(user) do
old_follower_address = User.ap_followers(user)



+ 69
- 16
lib/pleroma/web/activity_pub/utils.ex View File

@@ -1,11 +1,13 @@
defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.{Repo, Web, Object, Activity, User}
alias Pleroma.{Repo, Web, Object, Activity, User, Notification}
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Endpoint
alias Ecto.{Changeset, UUID}
import Ecto.Query
require Logger

@supported_object_types ["Article", "Note", "Video", "Page"]

# Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have.
def get_ap_id(object) do
@@ -19,22 +21,58 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Map.put(params, "actor", get_ap_id(params["actor"]))
end

defp recipient_in_collection(ap_id, coll) when is_binary(coll), do: ap_id == coll
defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll
defp recipient_in_collection(_, _), do: false

def recipient_in_message(ap_id, params) do
cond do
recipient_in_collection(ap_id, params["to"]) ->
true

recipient_in_collection(ap_id, params["cc"]) ->
true

recipient_in_collection(ap_id, params["bto"]) ->
true

recipient_in_collection(ap_id, params["bcc"]) ->
true

# if the message is unaddressed at all, then assume it is directly addressed
# to the recipient
!params["to"] && !params["cc"] && !params["bto"] && !params["bcc"] ->
true

true ->
false
end
end

defp extract_list(target) when is_binary(target), do: [target]
defp extract_list(lst) when is_list(lst), do: lst
defp extract_list(_), do: []

def maybe_splice_recipient(ap_id, params) do
need_splice =
!recipient_in_collection(ap_id, params["to"]) &&
!recipient_in_collection(ap_id, params["cc"])

cc_list = extract_list(params["cc"])

if need_splice do
params
|> Map.put("cc", [ap_id | cc_list])
else
params
end
end

def make_json_ld_header do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
%{
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
"sensitive" => "as:sensitive",
"Hashtag" => "as:Hashtag",
"ostatus" => "http://ostatus.org#",
"atomUri" => "ostatus:atomUri",
"inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
"conversation" => "ostatus:conversation",
"toot" => "http://joinmastodon.org/ns#",
"Emoji" => "toot:Emoji"
}
"#{Web.base_url()}/schemas/litepub-0.1.jsonld"
]
}
end
@@ -59,6 +97,21 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"#{Web.base_url()}/#{type}/#{UUID.generate()}"
end

def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
fake_create_activity = %{
"to" => object["to"],
"cc" => object["cc"],
"type" => "Create",
"object" => object
}

Notification.get_notified_from_activity(%Activity{data: fake_create_activity}, false)
end

def get_notified_from_object(object) do
Notification.get_notified_from_activity(%Activity{data: object}, false)
end

def create_context(context) do
context = context || generate_id("contexts")
changeset = Object.context_mapping(context)
@@ -128,7 +181,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data})
when is_map(object_data) and type in ["Article", "Note", "Video"] do
when is_map(object_data) and type in @supported_object_types do
with {:ok, _} <- Object.create(object_data) do
:ok
end
@@ -247,11 +300,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"actor" => follower_id,
"to" => [followed_id],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => followed_id
"object" => followed_id,
"state" => "pending"
}

data = if activity_id, do: Map.put(data, "id", activity_id), else: data
data = if User.locked?(followed), do: Map.put(data, "state", "pending"), else: data

data
end


+ 25
- 18
lib/pleroma/web/activity_pub/views/object_view.ex View File

@@ -1,27 +1,34 @@
defmodule Pleroma.Web.ActivityPub.ObjectView do
use Pleroma.Web, :view
alias Pleroma.{Object, Activity}
alias Pleroma.Web.ActivityPub.Transmogrifier

def render("object.json", %{object: object}) do
base = %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
%{
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
"sensitive" => "as:sensitive",
"Hashtag" => "as:Hashtag",
"ostatus" => "http://ostatus.org#",
"atomUri" => "ostatus:atomUri",
"inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
"conversation" => "ostatus:conversation",
"toot" => "http://joinmastodon.org/ns#",
"Emoji" => "toot:Emoji"
}
]
}
def render("object.json", %{object: %Object{} = object}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()

additional = Transmogrifier.prepare_object(object.data)
Map.merge(base, additional)
end

def render("object.json", %{object: %Activity{data: %{"type" => "Create"}} = 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

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", object.data["id"])

Map.merge(base, additional)
end
end

+ 1
- 1
lib/pleroma/web/activity_pub/views/user_view.ex View File

@@ -17,7 +17,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
public_key = :public_key.pem_encode([public_key])

%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => user.ap_id,
"type" => "Application",
"following" => "#{user.ap_id}/following",
@@ -36,6 +35,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
}
}
|> Map.merge(Utils.make_json_ld_header())
end

def render("user.json", %{user: user}) do


+ 158
- 0
lib/pleroma/web/admin_api/admin_api_controller.ex View File

@@ -0,0 +1,158 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller
alias Pleroma.{User, Repo}
alias Pleroma.Web.ActivityPub.Relay

require Logger

action_fallback(:errors)

def user_delete(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname)

if user.local == true do
User.delete(user)
else
User.delete(user)
end

conn
|> json(nickname)
end

def user_create(
conn,
%{"nickname" => nickname, "email" => email, "password" => password}
) do
new_user = %{
nickname: nickname,
name: nickname,
email: email,
password: password,
password_confirmation: password,
bio: "."
}

User.register_changeset(%User{}, new_user)
|> Repo.insert!()

conn
|> json(new_user.nickname)
end

def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
when permission_group in ["moderator", "admin"] do
user = User.get_by_nickname(nickname)

info =
user.info
|> Map.put("is_" <> permission_group, true)

cng = User.info_changeset(user, %{info: info})
{:ok, user} = User.update_and_set_cache(cng)

conn
|> json(user.info)
end

def right_get(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname)

conn
|> json(user.info)
end

def right_add(conn, _) do
conn
|> put_status(404)
|> json(%{error: "No such permission_group"})
end

def right_delete(
%{assigns: %{user: %User{:nickname => admin_nickname}}} = conn,
%{
"permission_group" => permission_group,
"nickname" => nickname
}
)
when permission_group in ["moderator", "admin"] do
if admin_nickname == nickname do
conn
|> put_status(403)
|> json(%{error: "You can't revoke your own admin status."})
else
user = User.get_by_nickname(nickname)

info =
user.info
|> Map.put("is_" <> permission_group, false)

cng = User.info_changeset(user, %{info: info})
{:ok, user} = User.update_and_set_cache(cng)

conn
|> json(user.info)
end
end

def right_delete(conn, _) do
conn
|> put_status(404)
|> json(%{error: "No such permission_group"})
end

def relay_follow(conn, %{"relay_url" => target}) do
{status, message} = Relay.follow(target)

if status == :ok do
conn
|> json(target)
else
conn
|> put_status(500)
|> json(target)
end
end

def relay_unfollow(conn, %{"relay_url" => target}) do
{status, message} = Relay.unfollow(target)

if status == :ok do
conn
|> json(target)
else
conn
|> put_status(500)
|> json(target)
end
end

@shortdoc "Get a account registeration invite token (base64 string)"
def get_invite_token(conn, _params) do
{:ok, token} = Pleroma.UserInviteToken.create_token()

conn
|> json(token.token)
end

@shortdoc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_by_nickname(nickname)
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)

conn
|> json(token.token)
end

def errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
end

def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
end

+ 3
- 4
lib/pleroma/web/channels/user_socket.ex View File

@@ -4,9 +4,7 @@ defmodule Pleroma.Web.UserSocket do

## Channels
# channel "room:*", Pleroma.Web.RoomChannel
if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do
channel("chat:*", Pleroma.Web.ChatChannel)
end
channel("chat:*", Pleroma.Web.ChatChannel)

## Transports
transport(:websocket, Phoenix.Transports.WebSocket)
@@ -24,7 +22,8 @@ defmodule Pleroma.Web.UserSocket do
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
def connect(%{"token" => token}, socket) do
with {:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84600),
with true <- Pleroma.Config.get([:chat, :enabled]),
{:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84600),
%User{} = user <- Pleroma.Repo.get(User, user_id) do
{:ok, assign(socket, :user_name, user.nickname)}
else


+ 19
- 6
lib/pleroma/web/common_api/common_api.ex View File

@@ -36,7 +36,6 @@ defmodule Pleroma.Web.CommonAPI do

def favorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
false <- activity.data["actor"] == user.ap_id,
object <- Object.normalize(activity.data["object"]["id"]) do
ActivityPub.like(user, object)
else
@@ -47,7 +46,6 @@ defmodule Pleroma.Web.CommonAPI do

def unfavorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
false <- activity.data["actor"] == user.ap_id,
object <- Object.normalize(activity.data["object"]["id"]) do
ActivityPub.unlike(user, object)
else
@@ -72,22 +70,37 @@ defmodule Pleroma.Web.CommonAPI do

def get_visibility(_), do: "public"

@instance Application.get_env(:pleroma, :instance)
@limit Keyword.get(@instance, :limit)
defp get_content_type(content_type) do
if Enum.member?(Pleroma.Config.get([:instance, :allowed_post_formats]), content_type) do
content_type
else
"text/plain"
end
end

def post(user, %{"status" => status} = data) do
visibility = get_visibility(data)
limit = Pleroma.Config.get([:instance, :limit])

with status <- String.trim(status),
length when length in 1..@limit <- String.length(status),
attachments <- attachments_from_ids(data["media_ids"]),
mentions <- Formatter.parse_mentions(status),
inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]),
{to, cc} <- to_for_user_and_mentions(user, mentions, inReplyTo, visibility),
tags <- Formatter.parse_tags(status, data),
content_html <-
make_content_html(status, mentions, attachments, tags, data["no_attachment_links"]),
make_content_html(
status,
mentions,
attachments,
tags,
get_content_type(data["content_type"]),
data["no_attachment_links"]
),
context <- make_context(inReplyTo),
cw <- data["spoiler_text"],
full_payload <- String.trim(status <> (data["spoiler_text"] || "")),
length when length in 1..limit <- String.length(full_payload),
object <-
make_note_data(
user.ap_id,


+ 51
- 12
lib/pleroma/web/common_api/utils.ex View File

@@ -2,6 +2,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.{Repo, Object, Formatter, Activity}
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy
alias Pleroma.User
alias Calendar.Strftime
alias Comeonin.Pbkdf2
@@ -18,6 +19,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end

def get_replied_to_activity(""), do: nil

def get_replied_to_activity(id) when not is_nil(id) do
Repo.get(Activity, id)
end
@@ -31,21 +34,29 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end

def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
to = ["https://www.w3.org/ns/activitystreams#Public"]

mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
cc = [user.follower_address | mentioned_users]

to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users]
cc = [user.follower_address]

if inReplyTo do
{to, Enum.uniq([inReplyTo.data["actor"] | cc])}
{Enum.uniq([inReplyTo.data["actor"] | to]), cc}
else
{to, cc}
end
end

def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
{to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "public")
{cc, to}
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)

to = [user.follower_address | mentioned_users]
cc = ["https://www.w3.org/ns/activitystreams#Public"]

if inReplyTo do
{Enum.uniq([inReplyTo.data["actor"] | to]), cc}
else
{to, cc}
end
end

def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
@@ -63,9 +74,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end

def make_content_html(status, mentions, attachments, tags, no_attachment_links \\ false) do
def make_content_html(
status,
mentions,
attachments,
tags,
content_type,
no_attachment_links \\ false
) do
status
|> format_input(mentions, tags)
|> format_input(mentions, tags, content_type)
|> maybe_add_attachments(attachments, no_attachment_links)
end

@@ -81,8 +99,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def add_attachments(text, attachments) do
attachment_text =
Enum.map(attachments, fn
%{"url" => [%{"href" => href} | _]} ->
name = URI.decode(Path.basename(href))
%{"url" => [%{"href" => href} | _]} = attachment ->
name = attachment["name"] || URI.decode(Path.basename(href))
href = MediaProxy.url(href)
"<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"

_ ->
@@ -92,9 +111,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do
Enum.join([text | attachment_text], "<br>")
end

def format_input(text, mentions, tags) do
def format_input(text, mentions, tags, "text/plain") do
text
|> Formatter.html_escape()
|> Formatter.html_escape("text/plain")
|> String.replace(~r/\r?\n/, "<br>")
|> (&{[], &1}).()
|> Formatter.add_links()
@@ -103,6 +122,26 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.finalize()
end

def format_input(text, mentions, tags, "text/html") do
text
|> Formatter.html_escape("text/html")
|> String.replace(~r/\r?\n/, "<br>")
|> (&{[], &1}).()
|> Formatter.add_user_links(mentions)
|> Formatter.finalize()
end

def format_input(text, mentions, tags, "text/markdown") do
text
|> Earmark.as_html!()
|> Formatter.html_escape("text/html")
|> String.replace(~r/\r?\n/, "")
|> (&{[], &1}).()
|> Formatter.add_user_links(mentions)
|> Formatter.add_hashtag_links(tags)
|> Formatter.finalize()
end

def add_tag_links(text, tags) do
tags =
tags


+ 13
- 6
lib/pleroma/web/endpoint.ex View File

@@ -1,9 +1,7 @@
defmodule Pleroma.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :pleroma

if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do
socket("/socket", Pleroma.Web.UserSocket)
end
socket("/socket", Pleroma.Web.UserSocket)

socket("/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket)

@@ -11,13 +9,17 @@ defmodule Pleroma.Web.Endpoint do
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug(CORSPlug)
plug(Pleroma.Plugs.HTTPSecurityPlug)

plug(Plug.Static, at: "/media", from: Pleroma.Uploaders.Local.upload_path(), gzip: false)

plug(
Plug.Static,
at: "/",
from: :pleroma,
only: ~w(index.html static finmoji emoji packs sounds images instance sw.js favicon.png)
only:
~w(index.html static finmoji emoji packs sounds images instance sw.js favicon.png schemas)
)

# Code reloading can be explicitly enabled under the
@@ -42,14 +44,19 @@ defmodule Pleroma.Web.Endpoint do
plug(Plug.MethodOverride)
plug(Plug.Head)

cookie_name =
if Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag),
do: "__Host-pleroma_key",
else: "pleroma_key"

# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug(
Plug.Session,
store: :cookie,
key: "_pleroma_key",
signing_salt: "CqaoopA2",
key: cookie_name,
signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},
http_only: true,
secure:
Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag),


+ 44
- 36
lib/pleroma/web/federator/federator.ex View File

@@ -3,17 +3,17 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.User
alias Pleroma.Activity
alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.Federator.RetryQueue
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.OStatus
require Logger

@websub Application.get_env(:pleroma, :websub)
@ostatus Application.get_env(:pleroma, :ostatus)
@httpoison Application.get_env(:pleroma, :httpoison)
@instance Application.get_env(:pleroma, :instance)
@federating Keyword.get(@instance, :federating)
@max_jobs 20

def init(args) do
@@ -65,15 +65,17 @@ defmodule Pleroma.Web.Federator do
{:ok, actor} = WebFinger.ensure_keys_present(actor)

if ActivityPub.is_public?(activity) do
Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end)
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
if OStatus.is_representable?(activity) do
Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end)
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)

Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end)
Pleroma.Web.Salmon.publish(actor, activity)
Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end)
Pleroma.Web.Salmon.publish(actor, activity)
end

if Mix.env() != :test do
if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Pleroma.Web.ActivityPub.Relay.publish(activity)
Relay.publish(activity)
end
end

@@ -100,44 +102,46 @@ defmodule Pleroma.Web.Federator do

params = Utils.normalize_params(params)

# NOTE: we use the actor ID to do the containment, this is fine because an
# actor shouldn't be acting on objects outside their own AP server.
with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.normalize(params["id"]),
{:ok, _activity} <- Transmogrifier.handle_incoming(params) do
:ok <- Transmogrifier.contain_origin_from_id(params["actor"], params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, activity}
else
%Activity{} ->
Logger.info("Already had #{params["id"]}")
:error

_e ->
# Just drop those for now
Logger.info("Unhandled activity")
Logger.info(Poison.encode!(params, pretty: 2))
:error
end
end

def handle(:publish_single_ap, params) do
ActivityPub.publish_one(params)
end

def handle(:publish_single_websub, %{xml: xml, topic: topic, callback: callback, secret: secret}) do
signature = @websub.sign(secret || "", xml)
Logger.debug(fn -> "Pushing #{topic} to #{callback}" end)

with {:ok, %{status_code: code}} <-
@httpoison.post(
callback,
xml,
[
{"Content-Type", "application/atom+xml"},
{"X-Hub-Signature", "sha1=#{signature}"}
],
timeout: 10000,
recv_timeout: 20000,
hackney: [pool: :default]
) do
Logger.debug(fn -> "Pushed to #{callback}, code #{code}" end)
else
e ->
Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(e)}" end)
case ActivityPub.publish_one(params) do
{:ok, _} ->
:ok

{:error, _} ->
RetryQueue.enqueue(params, ActivityPub)
end
end

def handle(
:publish_single_websub,
%{xml: xml, topic: topic, callback: callback, secret: secret} = params
) do
case Websub.publish_one(params) do
{:ok, _} ->
:ok

{:error, _} ->
RetryQueue.enqueue(params, Websub)
end
end

@@ -146,11 +150,15 @@ defmodule Pleroma.Web.Federator do
{:error, "Don't know what to do with this"}
end

def enqueue(type, payload, priority \\ 1) do
if @federating do
if Mix.env() == :test do
if Mix.env() == :test do
def enqueue(type, payload, priority \\ 1) do
if Pleroma.Config.get([:instance, :federating]) do
handle(type, payload)
else
end
end
else
def enqueue(type, payload, priority \\ 1) do
if Pleroma.Config.get([:instance, :federating]) do
GenServer.cast(__MODULE__, {:enqueue, type, payload, priority})
end
end


+ 71
- 0
lib/pleroma/web/federator/retry_queue.ex View File

@@ -0,0 +1,71 @@
defmodule Pleroma.Web.Federator.RetryQueue do
use GenServer
alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.ActivityPub.ActivityPub
require Logger

@websub Application.get_env(:pleroma, :websub)
@ostatus Application.get_env(:pleroma, :websub)
@httpoison Application.get_env(:pleroma, :websub)
@instance Application.get_env(:pleroma, :websub)
# initial timeout, 5 min
@initial_timeout 30_000
@max_retries 5

def init(args) do
{:ok, args}
end

def start_link() do
GenServer.start_link(__MODULE__, %{delivered: 0, dropped: 0}, name: __MODULE__)
end

def enqueue(data, transport, retries \\ 0) do
GenServer.cast(__MODULE__, {:maybe_enqueue, data, transport, retries + 1})
end

def get_retry_params(retries) do
if retries > @max_retries do
{:drop, "Max retries reached"}
else
{:retry, growth_function(retries)}
end
end

def handle_cast({:maybe_enqueue, data, transport, retries}, %{dropped: drop_count} = state) do
case get_retry_params(retries) do
{:retry, timeout} ->
Process.send_after(
__MODULE__,
{:send, data, transport, retries},
growth_function(retries)
)

{:noreply, state}

{:drop, message} ->
Logger.debug(message)
{:noreply, %{state | dropped: drop_count + 1}}
end
end

def handle_info({:send, data, transport, retries}, %{delivered: delivery_count} = state) do
case transport.publish_one(data) do
{:ok, _} ->
{:noreply, %{state | delivered: delivery_count + 1}}

{:error, reason} ->
enqueue(data, transport, retries)
{:noreply, state}
end
end

def handle_info(unknown, state) do
Logger.debug("RetryQueue: don't know what to do with #{inspect(unknown)}, ignoring")
{:noreply, state}
end

defp growth_function(retries) do
round(@initial_timeout * :math.pow(retries, 3))
end
end

+ 102
- 48
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex View File

@@ -35,6 +35,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def update_credentials(%{assigns: %{user: user}} = conn, params) do
original_user = user

avatar_upload_limit =
Application.get_env(:pleroma, :instance)
|> Keyword.fetch(:avatar_upload_limit)

banner_upload_limit =
Application.get_env(:pleroma, :instance)
|> Keyword.fetch(:banner_upload_limit)

params =
if bio = params["note"] do
Map.put(params, "bio", bio)
@@ -52,7 +60,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
user =
if avatar = params["avatar"] do
with %Plug.Upload{} <- avatar,
{:ok, object} <- ActivityPub.upload(avatar),
{:ok, object} <- ActivityPub.upload(avatar, avatar_upload_limit),
change = Ecto.Changeset.change(user, %{avatar: object.data}),
{:ok, user} = User.update_and_set_cache(change) do
user
@@ -66,7 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
user =
if banner = params["header"] do
with %Plug.Upload{} <- banner,
{:ok, object} <- ActivityPub.upload(banner),
{:ok, object} <- ActivityPub.upload(banner, banner_upload_limit),
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
@@ -124,22 +132,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end

@instance Application.get_env(:pleroma, :instance)
@mastodon_api_level "2.5.0"

def masto_instance(conn, _params) do
instance = Pleroma.Config.get(:instance)

response = %{
uri: Web.base_url(),
title: Keyword.get(@instance, :name),
description: Keyword.get(@instance, :description),
version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
email: Keyword.get(@instance, :email),
title: Keyword.get(instance, :name),
description: Keyword.get(instance, :description),
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
email: Keyword.get(instance, :email),
urls: %{
streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
},
stats: Stats.get_stats(),
thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
max_toot_chars: Keyword.get(@instance, :limit)
max_toot_chars: Keyword.get(instance, :limit)
}

json(conn, response)
@@ -150,7 +159,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end

defp mastodonized_emoji do
Pleroma.Formatter.get_custom_emoji()
Pleroma.Emoji.get_all()
|> Enum.map(fn {shortcode, relative_url} ->
url = to_string(URI.merge(Web.base_url(), relative_url))

@@ -223,6 +232,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do

activities =
ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> ActivityPub.contain_timeline(user)
|> Enum.reverse()

conn
@@ -268,9 +278,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end

def dm_timeline(%{assigns: %{user: user}} = conn, _params) do
def dm_timeline(%{assigns: %{user: user}} = conn, params) do
query =
ActivityPub.fetch_activities_query([user.ap_id], %{"type" => "Create", visibility: "direct"})
ActivityPub.fetch_activities_query(
[user.ap_id],
Map.merge(params, %{"type" => "Create", visibility: "direct"})
)

activities = Repo.all(query)

@@ -282,7 +295,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do
render(conn, StatusView, "status.json", %{activity: activity, for: user})
try_render(conn, StatusView, "status.json", %{activity: activity, for: user})
end
end

@@ -345,7 +358,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
{:ok, activity} =
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)

render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end

def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
@@ -361,28 +374,28 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do

def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
try_render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
end
end

def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end

def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end

def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end

@@ -434,6 +447,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
end

# Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
def relationships(%{assigns: %{user: user}} = conn, _) do
conn
|> json([])
end

def update_media(%{assigns: %{user: _}} = conn, data) do
with %Object{} = object <- Repo.get(Object, data["id"]),
true <- is_binary(data["description"]),
@@ -499,6 +518,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("tag", String.downcase(params["tag"]))

activities =
ActivityPub.fetch_public_activities(params)
@@ -574,7 +594,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do
{:ok, _activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
else
{:error, message} ->
@@ -765,6 +791,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end

def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
lists = Pleroma.List.get_lists_account_belongs(user, account_id)
res = ListView.render("lists.json", lists: lists)
json(conn, res)
end

def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, _list} <- Pleroma.List.delete(list) do
@@ -859,6 +891,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
if user && token do
mastodon_emoji = mastodonized_emoji()

limit = Pleroma.Config.get([:instance, :limit])

accounts =
Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))

@@ -878,7 +912,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
auto_play_gif: false,
display_sensitive_media: false,
reduce_motion: false,
max_toot_chars: Keyword.get(@instance, :limit)
max_toot_chars: limit
},
rights: %{
delete_others_notice: !!user.info["is_moderator"]
@@ -938,7 +972,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
push_subscription: nil,
accounts: accounts,
custom_emojis: mastodon_emoji,
char_limit: Keyword.get(@instance, :limit)
char_limit: limit
}
|> Jason.encode!()

@@ -964,9 +998,29 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end

def login(conn, %{"code" => code}) do
with {:ok, app} <- get_or_make_app(),
%Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
{:ok, token} <- Token.exchange_token(app, auth) do
conn
|> put_session(:oauth_token, token.token)
|> redirect(to: "/web/getting-started")
end
end

def login(conn, _) do
conn
|> render(MastodonView, "login.html", %{error: false})
with {:ok, app} <- get_or_make_app() do
path =
o_auth_path(conn, :authorize,
response_type: "code",
client_id: app.client_id,
redirect_uri: ".",
scope: app.scopes
)

conn
|> redirect(to: path)
end
end

defp get_or_make_app() do
@@ -985,22 +1039,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end

def login_post(conn, %{"authorization" => %{"name" => name, "password" => password}}) do
with %User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
{:ok, app} <- get_or_make_app(),
{:ok, auth} <- Authorization.create_authorization(app, user),
{:ok, token} <- Token.exchange_token(app, auth) do
conn
|> put_session(:oauth_token, token.token)
|> redirect(to: "/web/getting-started")
else
_e ->
conn
|> render(MastodonView, "login.html", %{error: "Wrong username or password"})
end
end

def logout(conn, _) do
conn
|> clear_session
@@ -1173,18 +1211,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> json("Something went wrong")
end

@suggestions Application.get_env(:pleroma, :suggestions)

def suggestions(%{assigns: %{user: user}} = conn, _) do
if Keyword.get(@suggestions, :enabled, false) do
api = Keyword.get(@suggestions, :third_party_engine, "")
timeout = Keyword.get(@suggestions, :timeout, 5000)
limit = Keyword.get(@suggestions, :limit, 23)
suggestions = Pleroma.Config.get(:suggestions)

if Keyword.get(suggestions, :enabled, false) do
api = Keyword.get(suggestions, :third_party_engine, "")
timeout = Keyword.get(suggestions, :timeout, 5000)
limit = Keyword.get(suggestions, :limit, 23)

host =
Application.get_env(:pleroma, Pleroma.Web.Endpoint)
|> Keyword.get(:url)
|> Keyword.get(:host)
host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])

user = user.nickname
url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
@@ -1220,4 +1255,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, [])
end
end

def try_render(conn, renderer, target, params)
when is_binary(target) do
res = render(conn, renderer, target, params)

if res == nil do
conn
|> put_status(501)
|> json(%{error: "Can't display this activity"})
else
res
end
end

def try_render(conn, _, _, _) do
conn
|> put_status(501)
|> json(%{error: "Can't display this activity"})
end
end

+ 27
- 6
lib/pleroma/web/mastodon_api/mastodon_socket.ex View File

@@ -11,9 +11,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonSocket do
timeout: :infinity
)

def connect(params, socket) do
with token when not is_nil(token) <- params["access_token"],
%Token{user_id: user_id} <- Repo.get_by(Token, token: token),
def connect(%{"access_token" => token} = params, socket) do
with %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
%User{} = user <- Repo.get(User, user_id),
stream
when stream in [
@@ -26,15 +25,37 @@ defmodule Pleroma.Web.MastodonAPI.MastodonSocket do
"list",
"hashtag"
] <- params["stream"] do
topic = if stream == "list", do: "list:#{params["list"]}", else: stream
socket_stream = if stream == "hashtag", do: "hashtag:#{params["tag"]}", else: stream
topic =
case stream do
"hashtag" -> "hashtag:#{params["tag"]}"
"list" -> "list:#{params["list"]}"
_ -> stream
end

socket =
socket
|> assign(:topic, topic)
|> assign(:user, user)

Pleroma.Web.Streamer.add_socket(socket_stream, socket)
Pleroma.Web.Streamer.add_socket(topic, socket)
{:ok, socket}
else
_e -> :error
end
end

def connect(%{"stream" => stream} = params, socket)
when stream in ["public", "public:local", "hashtag"] do
topic =
case stream do
"hashtag" -> "hashtag:#{params["tag"]}"
_ -> stream
end

with socket =
socket
|> assign(:topic, topic) do
Pleroma.Web.Streamer.add_socket(topic, socket)
{:ok, socket}
else
_e -> :error


+ 10
- 1
lib/pleroma/web/mastodon_api/views/account_view.ex View File

@@ -72,6 +72,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end

def render("relationship.json", %{user: user, target: target}) do
follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)

requested =
if follow_activity do
follow_activity.data["state"] == "pending"
else
false
end

%{
id: to_string(target.id),
following: User.following?(user, target),
@@ -79,7 +88,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
blocking: User.blocks?(user, target),
muting: false,
muting_notifications: false,
requested: false,
requested: requested,
domain_blocking: false,
showing_reblogs: false,
endorsed: false


+ 14
- 9
lib/pleroma/web/mastodon_api/views/status_view.ex View File

@@ -34,6 +34,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
"status.json",
Map.put(opts, :replied_to_activities, replied_to_activities)
)
|> Enum.filter(fn x -> not is_nil(x) end)
end

def render(
@@ -60,7 +61,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
in_reply_to_id: nil,
in_reply_to_account_id: nil,
reblog: reblogged,
content: reblogged[:content],
content: reblogged[:content] || "",
created_at: created_at,
reblogs_count: 0,
replies_count: 0,
@@ -158,10 +159,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end

def render("status.json", _) do
nil
end

def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
href = attachment_url["href"]
href = attachment_url["href"] |> MediaProxy.url()

type =
cond do
@@ -175,9 +180,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do

%{
id: to_string(attachment["id"] || hash_id),
url: MediaProxy.url(href),
url: href,
remote_url: href,
preview_url: MediaProxy.url(href),
preview_url: href,
text_url: href,
type: type,
description: attachment["name"]
@@ -225,24 +230,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
if !!name and name != "" do
"<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
else
object["content"]
object["content"] || ""
end

content
end

def render_content(%{"type" => "Article"} = object) do
def render_content(%{"type" => object_type} = object) when object_type in ["Article", "Page"] do
summary = object["name"]

content =
if !!summary and summary != "" do
if !!summary and summary != "" and is_bitstring(object["url"]) do
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
else
object["content"]
object["content"] || ""
end

content
end

def render_content(object), do: object["content"]
def render_content(object), do: object["content"] || ""
end

+ 41
- 3
lib/pleroma/web/media_proxy/controller.ex View File

@@ -11,15 +11,47 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
error: "public, must-revalidate, max-age=160"
}

def remote(conn, %{"sig" => sig, "url" => url}) do
# Content-types that will not be returned as content-disposition attachments
# Override with :media_proxy, :safe_content_types in the configuration
@safe_content_types [
"image/gif",
"image/jpeg",
"image/jpg",
"image/png",
"image/svg+xml",
"audio/mpeg",
"audio/mp3",
"video/webm",
"video/mp4"
]

def remote(conn, params = %{"sig" => sig, "url" => url}) do
config = Application.get_env(:pleroma, :media_proxy, [])

with true <- Keyword.get(config, :enabled, false),
{:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url),
{:ok, content_type, body} <- proxy_request(url) do
filename <- Path.basename(URI.parse(url).path),
true <-
if(Map.get(params, "filename"),
do: filename == Path.basename(conn.request_path),
else: true
),
{:ok, content_type, body} <- proxy_request(url),
safe_content_type <-
Enum.member?(
Keyword.get(config, :safe_content_types, @safe_content_types),
content_type
) do
conn
|> put_resp_content_type(content_type)
|> set_cache_header(:default)
|> put_resp_header(
"content-security-policy",
"default-src 'none'; style-src 'unsafe-inline'; media-src data:; img-src 'self' data:"
)
|> put_resp_header("x-xss-protection", "1; mode=block")
|> put_resp_header("x-content-type-options", "nosniff")
|> put_attachement_header(safe_content_type, filename)
|> send_resp(200, body)
else
false ->
@@ -92,6 +124,12 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
# TODO: the body is passed here as well because some hosts do not provide a content-type.
# At some point we may want to use magic numbers to discover the content-type and reply a proper one.
defp proxy_request_content_type(headers, _body) do
headers["Content-Type"] || headers["content-type"] || "image/jpeg"
headers["Content-Type"] || headers["content-type"] || "application/octet-stream"
end

defp put_attachement_header(conn, true, _), do: conn

defp put_attachement_header(conn, false, filename) do
put_resp_header(conn, "content-disposition", "attachment; filename='#{filename}'")
end
end

+ 6
- 1
lib/pleroma/web/media_proxy/media_proxy.ex View File

@@ -3,6 +3,8 @@ defmodule Pleroma.Web.MediaProxy do

def url(nil), do: nil

def url(""), do: nil

def url(url = "/" <> _), do: url

def url(url) do
@@ -15,7 +17,10 @@ defmodule Pleroma.Web.MediaProxy do
base64 = Base.url_encode64(url, @base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts)
Keyword.get(config, :base_url, Pleroma.Web.base_url()) <> "/proxy/#{sig64}/#{base64}"
filename = if path = URI.parse(url).path, do: "/" <> Path.basename(path), else: ""

Keyword.get(config, :base_url, Pleroma.Web.base_url()) <>
"/proxy/#{sig64}/#{base64}#{filename}"
end
end



+ 68
- 5
lib/pleroma/web/nodeinfo/nodeinfo_controller.ex View File

@@ -4,6 +4,10 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.Stats
alias Pleroma.Web
alias Pleroma.{User, Repo}
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF

plug(Pleroma.Web.FederatingPlug)

def schemas(conn, _params) do
response = %{
@@ -27,16 +31,69 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
gopher = Application.get_env(:pleroma, :gopher)
stats = Stats.get_stats()

mrf_simple =
Application.get_env(:pleroma, :mrf_simple)
|> Enum.into(%{})

mrf_policies =
MRF.get_policies()
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)

quarantined = Keyword.get(instance, :quarantined_instances)

quarantined =
if is_list(quarantined) do
quarantined
else
[]
end

staff_accounts =
User.moderator_user_query()
|> Repo.all()
|> Enum.map(fn u -> u.ap_id end)

mrf_user_allowlist =
Config.get([:mrf_user_allowlist], [])
|> Enum.into(%{}, fn {k, v} -> {k, length(v)} end)

mrf_transparency = Keyword.get(instance, :mrf_transparency)

federation_response =
if mrf_transparency do
%{
mrf_policies: mrf_policies,
mrf_simple: mrf_simple,
mrf_user_allowlist: mrf_user_allowlist,
quarantined_instances: quarantined
}
else
%{}
end

features = [
"pleroma_api",
"mastodon_api",
"mastodon_api_streaming",
if Keyword.get(media_proxy, :enabled) do
"media_proxy"
end,
if Keyword.get(gopher, :enabled) do
"gopher"
end,
if Keyword.get(chat, :enabled) do
"chat"
end,
if Keyword.get(suggestions, :enabled) do
"suggestions"
end
]

response = %{
version: "2.0",
software: %{
name: "pleroma",
version: Keyword.get(instance, :version)
name: Pleroma.Application.name(),
version: Pleroma.Application.version()
},
protocols: ["ostatus", "activitypub"],
services: %{
@@ -53,7 +110,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
metadata: %{
nodeName: Keyword.get(instance, :name),
nodeDescription: Keyword.get(instance, :description),
mediaProxy: Keyword.get(media_proxy, :enabled),
private: !Keyword.get(instance, :public, true),
suggestions: %{
enabled: Keyword.get(suggestions, :enabled, false),
@@ -63,8 +119,15 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
web: Keyword.get(suggestions, :web, "")
},
staffAccounts: staff_accounts,
chat: Keyword.get(chat, :enabled),
gopher: Keyword.get(gopher, :enabled)
federation: federation_response,
postFormats: Keyword.get(instance, :allowed_post_formats),
uploadLimits: %{
general: Keyword.get(instance, :upload_limit),
avatar: Keyword.get(instance, :avatar_upload_limit),
banner: Keyword.get(instance, :banner_upload_limit),
background: Keyword.get(instance, :background_upload_limit)
},
features: features
}
}



+ 9
- 1
lib/pleroma/web/oauth/authorization.ex View File

@@ -4,7 +4,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
alias Pleroma.{User, Repo}
alias Pleroma.Web.OAuth.{Authorization, App}

import Ecto.{Changeset}
import Ecto.{Changeset, Query}

schema "oauth_authorizations" do
field(:token, :string)
@@ -45,4 +45,12 @@ defmodule Pleroma.Web.OAuth.Authorization do
end

def use_token(%Authorization{used: true}), do: {:error, "already used"}

def delete_user_authorizations(%User{id: user_id}) do
from(
a in Pleroma.Web.OAuth.Authorization,
where: a.user_id == ^user_id
)
|> Repo.delete_all()
end
end

+ 32
- 19
lib/pleroma/web/oauth/oauth_controller.ex View File

@@ -33,25 +33,35 @@ defmodule Pleroma.Web.OAuth.OAuthController do
true <- Pbkdf2.checkpw(password, user.password_hash),
%App{} = app <- Repo.get_by(App, client_id: client_id),
{:ok, auth} <- Authorization.create_authorization(app, user) do
if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do
render(conn, "results.html", %{
auth: auth
})
else
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
url = "#{redirect_uri}#{connector}"
url_params = %{:code => auth.token}

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)
# Special case: Local MastodonFE.
redirect_uri =
if redirect_uri == "." do
mastodon_api_url(conn, :login)
else
redirect_uri
end

cond do
redirect_uri == "urn:ietf:wg:oauth:2.0:oob" ->
render(conn, "results.html", %{
auth: auth
})

true ->
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
url = "#{redirect_uri}#{connector}"
url_params = %{:code => auth.token}

url_params =
if params["state"] do
Map.put(url_params, :state, params["state"])
else
url_params
end

url = "#{url}#{Plug.Conn.Query.encode(url_params)}"

redirect(conn, external: url)
end
end
end
@@ -133,8 +143,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end

# XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be
# decoding it. Investigate sometime.
defp fix_padding(token) do
token
|> URI.decode()
|> Base.url_decode64!(padding: false)
|> Base.url_encode64()
end


+ 10
- 0
lib/pleroma/web/oauth/token.ex View File

@@ -1,6 +1,8 @@
defmodule Pleroma.Web.OAuth.Token do
use Ecto.Schema

import Ecto.Query

alias Pleroma.{User, Repo}
alias Pleroma.Web.OAuth.{Token, App, Authorization}

@@ -35,4 +37,12 @@ defmodule Pleroma.Web.OAuth.Token do

Repo.insert(token)
end

def delete_user_tokens(%User{id: user_id}) do
from(
t in Pleroma.Web.OAuth.Token,
where: t.user_id == ^user_id
)
|> Repo.delete_all()
end
end

+ 15
- 0
lib/pleroma/web/ostatus/ostatus.ex View File

@@ -11,6 +11,21 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler}
alias Pleroma.Web.ActivityPub.Transmogrifier

def is_representable?(%Activity{data: data}) do
object = Object.normalize(data["object"])

cond do
is_nil(object) ->
false

object.data["type"] == "Note" ->
true

true ->
false
end
end

def feed_path(user) do
"#{user.ap_id}/feed.atom"
end


+ 16
- 4
lib/pleroma/web/ostatus/ostatus_controller.ex View File

@@ -1,7 +1,7 @@
defmodule Pleroma.Web.OStatus.OStatusController do
use Pleroma.Web, :controller

alias Pleroma.{User, Activity}
alias Pleroma.{User, Activity, Object}
alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter}
alias Pleroma.Repo
alias Pleroma.Web.{OStatus, Federator}
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ActivityPub

plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
action_fallback(:errors)

def feed_redirect(conn, %{"nickname" => nickname}) do
@@ -135,7 +136,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
"html" ->
conn
|> put_resp_content_type("text/html")
|> send_file(200, "priv/static/index.html")
|> send_file(200, Application.app_dir(:pleroma, "priv/static/index.html"))

_ ->
represent_activity(conn, format, activity, user)
@@ -152,10 +153,21 @@ defmodule Pleroma.Web.OStatus.OStatusController do
end
end

defp represent_activity(conn, "activity+json", activity, user) do
defp represent_activity(
conn,
"activity+json",
%Activity{data: %{"type" => "Create"}} = activity,
user
) do
object = Object.normalize(activity.data["object"])

conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ObjectView.render("object.json", %{object: activity}))
|> json(ObjectView.render("object.json", %{object: object}))
end

defp represent_activity(conn, "activity+json", _, _) do
{:error, :not_found}
end

defp represent_activity(conn, _, activity, user) do


+ 70
- 45
lib/pleroma/web/router.ex View File

@@ -3,12 +3,6 @@ defmodule Pleroma.Web.Router do

alias Pleroma.{Repo, User, Web.Router}

@instance Application.get_env(:pleroma, :instance)
@federating Keyword.get(@instance, :federating)
@allow_relay Keyword.get(@instance, :allow_relay)
@public Keyword.get(@instance, :public)
@registrations_open Keyword.get(@instance, :registrations_open)

pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
@@ -37,6 +31,21 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
end

pipeline :admin_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.UserIsAdminPlug)
end

pipeline :mastodon_html do
plug(:accepts, ["html"])
plug(:fetch_session)
@@ -85,6 +94,23 @@ defmodule Pleroma.Web.Router do
get("/emoji", UtilController, :emoji)
end

scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
delete("/user", AdminAPIController, :user_delete)
post("/user", AdminAPIController, :user_create)

get("/permission_group/:nickname", AdminAPIController, :right_get)
get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get)
post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add)
delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete)

post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)

get("/invite_token", AdminAPIController, :get_invite_token)
get("/password_reset", AdminAPIController, :get_password_reset)
end

scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
get("/ostatus_subscribe", UtilController, :remote_follow)
@@ -119,6 +145,7 @@ defmodule Pleroma.Web.Router do
post("/accounts/:id/unblock", MastodonAPIController, :unblock)
post("/accounts/:id/mute", MastodonAPIController, :relationship_noop)
post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop)
get("/accounts/:id/lists", MastodonAPIController, :account_lists)

get("/follow_requests", MastodonAPIController, :follow_requests)
post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
@@ -247,11 +274,7 @@ defmodule Pleroma.Web.Router do
end

scope "/api", Pleroma.Web do
if @public do
pipe_through(:api)
else
pipe_through(:authenticated_api)
end
pipe_through(:api)

get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline)

@@ -264,7 +287,12 @@ defmodule Pleroma.Web.Router do
get("/statuses/networkpublic_timeline", TwitterAPI.Controller, :public_and_external_timeline)
end

scope "/api", Pleroma.Web do
scope "/api", Pleroma.Web, as: :twitter_api_search do
pipe_through(:api)
get("/pleroma/search_user", TwitterAPI.Controller, :search_user)
end

scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)

get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
@@ -284,8 +312,13 @@ defmodule Pleroma.Web.Router do
get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
get("/statuses/dm_timeline", TwitterAPI.Controller, :dm_timeline)
get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)

# XXX: this is really a pleroma API, but we want to keep the pleroma namespace clean
# for now.
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)

post("/statuses/update", TwitterAPI.Controller, :status_update)
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
@@ -335,12 +368,10 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/feed", OStatus.OStatusController, :feed)
get("/users/:nickname", OStatus.OStatusController, :feed_redirect)

if @federating do
post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
end
post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
end

pipeline :activitypub do
@@ -357,31 +388,27 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end

if @federating do
if @allow_relay do
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_relay)
get("/", ActivityPubController, :relay)
end
end
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_relay)
get("/", ActivityPubController, :relay)
end

scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
post("/inbox", ActivityPubController, :inbox)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
post("/inbox", ActivityPubController, :inbox)
end

scope "/.well-known", Pleroma.Web do
pipe_through(:well_known)
scope "/.well-known", Pleroma.Web do
pipe_through(:well_known)

get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end

scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end

scope "/", Pleroma.Web.MastodonAPI do
@@ -394,12 +421,12 @@ defmodule Pleroma.Web.Router do
end

pipeline :remote_media do
plug(:accepts, ["html"])
end

scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end

scope "/", Fallback do
@@ -414,11 +441,9 @@ defmodule Fallback.RedirectController do
use Pleroma.Web, :controller

def redirector(conn, _params) do
if Mix.env() != :test do
conn
|> put_resp_content_type("text/html")
|> send_file(200, "priv/static/index.html")
end
conn
|> put_resp_content_type("text/html")
|> send_file(200, Application.app_dir(:pleroma, "priv/static/index.html"))
end

def registration_page(conn, params) do


+ 33
- 11
lib/pleroma/web/streamer.ex View File

@@ -73,7 +73,8 @@ defmodule Pleroma.Web.Streamer do
Pleroma.List.get_lists_from_activity(item)
|> Enum.filter(fn list ->
owner = Repo.get(User, list.user_id)
author.follower_address in owner.following

ActivityPub.visible_for_user?(item, owner)
end)
end

@@ -169,16 +170,33 @@ defmodule Pleroma.Web.Streamer do
|> Jason.encode!()
end

defp represent_update(%Activity{} = activity) do
%{
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
"status.json",
activity: activity
)
|> Jason.encode!()
}
|> Jason.encode!()
end

def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do
Enum.each(topics[topic] || [], fn socket ->
# Get the current user so we have up-to-date blocks etc.
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info["blocks"] || []
if socket.assigns[:user] do
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info["blocks"] || []

parent = Object.normalize(item.data["object"])
parent = Object.normalize(item.data["object"])

unless is_nil(parent) or item.actor in blocks or parent.data["actor"] in blocks do
send(socket.transport_pid, {:text, represent_update(item, user)})
unless is_nil(parent) or item.actor in blocks or parent.data["actor"] in blocks do
send(socket.transport_pid, {:text, represent_update(item, user)})
end
else
send(socket.transport_pid, {:text, represent_update(item)})
end
end)
end
@@ -186,11 +204,15 @@ defmodule Pleroma.Web.Streamer do
def push_to_socket(topics, topic, item) do
Enum.each(topics[topic] || [], fn socket ->
# Get the current user so we have up-to-date blocks etc.
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info["blocks"] || []

unless item.actor in blocks do
send(socket.transport_pid, {:text, represent_update(item, user)})
if socket.assigns[:user] do
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info["blocks"] || []

unless item.actor in blocks do
send(socket.transport_pid, {:text, represent_update(item, user)})
end
else
send(socket.transport_pid, {:text, represent_update(item)})
end
end)
end


+ 3
- 1
lib/pleroma/web/templates/layout/app.html.eex View File

@@ -2,7 +2,9 @@
<html>
<head>
<meta charset=utf-8 />
<title>Pleroma</title>
<title>
<%= Application.get_env(:pleroma, :instance)[:name] %>
</title>
<style>
body {
background-color: #282c37;


+ 0
- 11
lib/pleroma/web/templates/mastodon_api/mastodon/login.html.eex 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 %>

+ 31
- 27
lib/pleroma/web/twitter_api/controllers/util_controller.ex View File

@@ -6,7 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Web.WebFinger
alias Pleroma.Web.CommonAPI
alias Comeonin.Pbkdf2
alias Pleroma.Formatter
alias Pleroma.{Formatter, Emoji}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.{Repo, PasswordResetToken, User}

@@ -134,19 +134,20 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end

@instance Application.get_env(:pleroma, :instance)
@instance_fe Application.get_env(:pleroma, :fe)
@instance_chat Application.get_env(:pleroma, :chat)
def config(conn, _params) do
instance = Pleroma.Config.get(:instance)
instance_fe = Pleroma.Config.get(:fe)
instance_chat = Pleroma.Config.get(:chat)

case get_format(conn) do
"xml" ->
response = """
<config>
<site>
<name>#{Keyword.get(@instance, :name)}</name>
<name>#{Keyword.get(instance, :name)}</name>
<site>#{Web.base_url()}</site>
<textlimit>#{Keyword.get(@instance, :limit)}</textlimit>
<closed>#{!Keyword.get(@instance, :registrations_open)}</closed>
<textlimit>#{Keyword.get(instance, :limit)}</textlimit>
<closed>#{!Keyword.get(instance, :registrations_open)}</closed>
</site>
</config>
"""
@@ -160,30 +161,33 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
Keyword.get(Application.get_env(:web_push_encryption, :vapid_details), :public_key)

data = %{
name: Keyword.get(@instance, :name),
description: Keyword.get(@instance, :description),
name: Keyword.get(instance, :name),
description: Keyword.get(instance, :description),
server: Web.base_url(),
textlimit: to_string(Keyword.get(@instance, :limit)),
closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1"),
private: if(Keyword.get(@instance, :public, true), do: "0", else: "1"),
textlimit: to_string(Keyword.get(instance, :limit)),
closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"),
private: if(Keyword.get(instance, :public, true), do: "0", else: "1"),
vapidPublicKey: vapid_public_key
}

pleroma_fe = %{
theme: Keyword.get(@instance_fe, :theme),
background: Keyword.get(@instance_fe, :background),
logo: Keyword.get(@instance_fe, :logo),
logoMask: Keyword.get(@instance_fe, :logo_mask),
logoMargin: Keyword.get(@instance_fe, :logo_margin),
redirectRootNoLogin: Keyword.get(@instance_fe, :redirect_root_no_login),
redirectRootLogin: Keyword.get(@instance_fe, :redirect_root_login),
chatDisabled: !Keyword.get(@instance_chat, :enabled),
showInstanceSpecificPanel: Keyword.get(@instance_fe, :show_instance_panel),
scopeOptionsEnabled: Keyword.get(@instance_fe, :scope_options_enabled),
collapseMessageWithSubject: Keyword.get(@instance_fe, :collapse_message_with_subject)
theme: Keyword.get(instance_fe, :theme),
background: Keyword.get(instance_fe, :background),
logo: Keyword.get(instance_fe, :logo),
logoMask: Keyword.get(instance_fe, :logo_mask),
logoMargin: Keyword.get(instance_fe, :logo_margin),
redirectRootNoLogin: Keyword.get(instance_fe, :redirect_root_no_login),
redirectRootLogin: Keyword.get(instance_fe, :redirect_root_login),
chatDisabled: !Keyword.get(instance_chat, :enabled),
showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel),
scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled),
formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled),
collapseMessageWithSubject: Keyword.get(instance_fe, :collapse_message_with_subject),
hidePostStats: Keyword.get(instance_fe, :hide_post_stats),
hideUserStats: Keyword.get(instance_fe, :hide_user_stats)
}

managed_config = Keyword.get(@instance, :managed_config)
managed_config = Keyword.get(instance, :managed_config)

data =
if managed_config do
@@ -197,7 +201,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end

def version(conn, _params) do
version = Keyword.get(@instance, :version)
version = Pleroma.Application.named_version()

case get_format(conn) do
"xml" ->
@@ -213,7 +217,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end

def emoji(conn, _params) do
json(conn, Enum.into(Formatter.get_custom_emoji(), %{}))
json(conn, Enum.into(Emoji.get_all(), %{}))
end

def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
@@ -226,7 +230,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
|> Enum.map(fn account ->
with %User{} = follower <- User.get_cached_by_ap_id(user.ap_id),
%User{} = followed <- User.get_or_fetch(account),
{:ok, follower} <- User.follow(follower, followed) do
{:ok, follower} <- User.maybe_direct_follow(follower, followed) do
ActivityPub.follow(follower, followed)
else
err -> Logger.debug("follow_import: following #{account} failed with #{inspect(err)}")


+ 8
- 0
lib/pleroma/web/twitter_api/representers/activity_representer.ex View File

@@ -180,6 +180,10 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do

attachments = (object["attachment"] || []) ++ video

reply_parent = Activity.get_in_reply_to_activity(activity)

reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)

%{
"id" => activity.id,
"uri" => activity.data["object"]["id"],
@@ -190,6 +194,10 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
"is_post_verb" => true,
"created_at" => created_at,
"in_reply_to_status_id" => object["inReplyToStatusId"],
"in_reply_to_screen_name" => reply_user && reply_user.nickname,
"in_reply_to_profileurl" => User.profile_url(reply_user),
"in_reply_to_ostatus_uri" => reply_user && reply_user.ap_id,
"in_reply_to_user_id" => reply_user && reply_user.id,
"statusnet_conversation_id" => conversation_id,
"attachments" => attachments |> ObjectRepresenter.enum_to_list(opts),
"attentions" => attentions,


+ 16
- 9
lib/pleroma/web/twitter_api/twitter_api.ex View File

@@ -3,11 +3,10 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.{OStatus, CommonAPI}
alias Pleroma.Web.MediaProxy
import Ecto.Query

@instance Application.get_env(:pleroma, :instance)
@httpoison Application.get_env(:pleroma, :httpoison)
@registrations_open Keyword.get(@instance, :registrations_open)

def create_status(%User{} = user, %{"status" => _} = data) do
CommonAPI.post(user, data)
@@ -23,7 +22,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
def follow(%User{} = follower, params) do
with {:ok, %User{} = followed} <- get_user(params),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed) do
{:ok, activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
{:ok, follower, followed, activity}
else
err -> err
@@ -92,7 +97,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
{:ok, object} = ActivityPub.upload(file)

url = List.first(object.data["url"])
href = url["href"]
href = url["href"] |> MediaProxy.url()
type = url["mediaType"]

case format do
@@ -133,18 +138,20 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
password_confirmation: params["confirm"]
}

registrations_open = Pleroma.Config.get([:instance, :registrations_open])

# no need to query DB if registration is open
token =
unless @registrations_open || is_nil(tokenString) do
unless registrations_open || is_nil(tokenString) do
Repo.get_by(UserInviteToken, %{token: tokenString})
end

cond do
@registrations_open || (!is_nil(token) && !token.used) ->
registrations_open || (!is_nil(token) && !token.used) ->
changeset = User.register_changeset(%User{}, params)

with {:ok, user} <- Repo.insert(changeset) do
!@registrations_open && UserInviteToken.mark_as_used(token.token)
!registrations_open && UserInviteToken.mark_as_used(token.token)
{:ok, user}
else
{:error, changeset} ->
@@ -155,10 +162,10 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
{:error, %{error: errors}}
end

!@registrations_open && is_nil(token) ->
!registrations_open && is_nil(token) ->
{:error, "Invalid token"}

!@registrations_open && token.used ->
!registrations_open && token.used ->
{:error, "Expired token"}
end
end


+ 65
- 5
lib/pleroma/web/twitter_api/twitter_api_controller.ex View File

@@ -11,6 +11,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do

require Logger

plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
action_fallback(:errors)

def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
@@ -79,7 +80,9 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> Map.put("blocking_user", user)
|> Map.put("user", user)

activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
activities =
ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> ActivityPub.contain_timeline(user)

conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
@@ -123,6 +126,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end

def dm_timeline(%{assigns: %{user: user}} = conn, params) do
query =
ActivityPub.fetch_activities_query(
[user.ap_id],
Map.merge(params, %{"type" => "Create", "user" => user, visibility: "direct"})
)

activities = Repo.all(query)

conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end

def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params)

@@ -130,6 +146,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
end

def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
Notification.set_read_up_to(user, latest_id)

notifications = Notification.for_user(user, params)

conn
|> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
end

def notifications_read(%{assigns: %{user: user}} = conn, _) do
bad_request_reply(conn, "You need to specify latest_id")
end

def follow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.follow(user, params) do
{:ok, user, followed, _activity} ->
@@ -261,7 +290,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end

def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, object} = ActivityPub.upload(params)
upload_limit =
Application.get_env(:pleroma, :instance)
|> Keyword.fetch(:avatar_upload_limit)

{:ok, object} = ActivityPub.upload(params, upload_limit)
change = Changeset.change(user, %{avatar: object.data})
{:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user)
@@ -270,7 +303,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end

def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}),
upload_limit =
Application.get_env(:pleroma, :instance)
|> Keyword.fetch(:banner_upload_limit)

with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, upload_limit),
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
@@ -284,7 +321,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end

def update_background(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(params),
upload_limit =
Application.get_env(:pleroma, :instance)
|> Keyword.fetch(:background_upload_limit)

with {:ok, object} <- ActivityPub.upload(params, upload_limit),
new_info <- Map.put(user.info, "background", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, _user} <- User.update_and_set_cache(change) do
@@ -423,7 +464,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
{String.trim(name, ":"), url}
end)

bio_html = CommonUtils.format_input(bio, mentions, tags)
bio_html = CommonUtils.format_input(bio, mentions, tags, "text/plain")
Map.put(params, "bio", bio_html |> Formatter.emojify(emoji))
else
params
@@ -488,6 +529,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end

def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
users = User.search(query, true)

conn
|> render(UserView, "index.json", %{users: users, for: user})
end

defp bad_request_reply(conn, error_message) do
json = error_json(conn, error_message)
json_reply(conn, 400, json)
@@ -504,6 +552,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
json_reply(conn, 403, json)
end

def only_if_public_instance(conn = %{conn: %{assigns: %{user: _user}}}, _), do: conn

def only_if_public_instance(conn, _) do
if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
conn
else
conn
|> forbidden_json_reply("Invalid credentials.")
|> halt()
end
end

defp error_json(conn, error_message) do
%{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
end


+ 10
- 2
lib/pleroma/web/twitter_api/views/activity_view.ex View File

@@ -236,6 +236,10 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
HTML.filter_tags(content, User.html_filter_policy(opts[:for]))
|> Formatter.emojify(object["emoji"])

reply_parent = Activity.get_in_reply_to_activity(activity)

reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)

%{
"id" => activity.id,
"uri" => activity.data["object"]["id"],
@@ -246,6 +250,10 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
"is_post_verb" => true,
"created_at" => created_at,
"in_reply_to_status_id" => object["inReplyToStatusId"],
"in_reply_to_screen_name" => reply_user && reply_user.nickname,
"in_reply_to_profileurl" => User.profile_url(reply_user),
"in_reply_to_ostatus_uri" => reply_user && reply_user.ap_id,
"in_reply_to_user_id" => reply_user && reply_user.id,
"statusnet_conversation_id" => conversation_id,
"attachments" => (object["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
"attentions" => attentions,
@@ -275,11 +283,11 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
{summary, content}
end

def render_content(%{"type" => "Article"} = object) do
def render_content(%{"type" => object_type} = object) when object_type in ["Article", "Page"] do
summary = object["name"] || object["summary"]

content =
if !!summary and summary != "" do
if !!summary and summary != "" and is_bitstring(object["url"]) do
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
else
object["content"]


+ 15
- 3
lib/pleroma/web/twitter_api/views/user_view.ex View File

@@ -37,6 +37,13 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
{String.trim(name, ":"), url}
end)

# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
# For example: [{"name": "Pronoun", "value": "she/her"}, …]
fields =
(user.info["source_data"]["attachment"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)

data = %{
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"description" => HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
@@ -48,8 +55,12 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
"statusnet_blocking" => statusnet_blocking,
"friends_count" => user_info[:following_count],
"id" => user.id,
"name" => user.name,
"name_html" => HTML.strip_tags(user.name) |> Formatter.emojify(emoji),
"name" => user.name || user.nickname,
"name_html" =>
if(user.name,
do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji),
else: user.nickname
),
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
@@ -65,7 +76,8 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
"is_local" => user.local,
"locked" => !!user.info["locked"],
"default_scope" => user.info["default_scope"] || "public",
"no_rich_text" => user.info["no_rich_text"] || false
"no_rich_text" => user.info["no_rich_text"] || false,
"fields" => fields
}

if assigns[:token] do


+ 2
- 0
lib/pleroma/web/web_finger/web_finger_controller.ex View File

@@ -3,6 +3,8 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do

alias Pleroma.Web.WebFinger

plug(Pleroma.Web.FederatingPlug)

def host_meta(conn, _params) do
xml = WebFinger.host_meta()



+ 25
- 0
lib/pleroma/web/websub/websub.ex View File

@@ -252,4 +252,29 @@ defmodule Pleroma.Web.Websub do
Pleroma.Web.Federator.enqueue(:request_subscription, sub)
end)
end

def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret}) do
signature = sign(secret || "", xml)
Logger.info(fn -> "Pushing #{topic} to #{callback}" end)

with {:ok, %{status_code: code}} <-
@httpoison.post(
callback,
xml,
[
{"Content-Type", "application/atom+xml"},
{"X-Hub-Signature", "sha1=#{signature}"}
],
timeout: 10000,
recv_timeout: 20000,
hackney: [pool: :default]
) do
Logger.info(fn -> "Pushed to #{callback}, code #{code}" end)
{:ok, code}
else
e ->
Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(e)}" end)
{:error, e}
end
end
end

+ 9
- 0
lib/pleroma/web/websub/websub_controller.ex View File

@@ -5,6 +5,15 @@ defmodule Pleroma.Web.Websub.WebsubController do
alias Pleroma.Web.Websub.WebsubClientSubscription
require Logger

plug(
Pleroma.Web.FederatingPlug
when action in [
:websub_subscription_request,
:websub_subscription_confirmation,
:websub_incoming
]
)

def websub_subscription_request(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname)



+ 64
- 2
mix.exs View File

@@ -4,13 +4,25 @@ defmodule Pleroma.Mixfile do
def project do
[
app: :pleroma,
version: "0.9.0",
version: version("0.9.0"),
elixir: "~> 1.4",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
deps: deps(),

# Docs
name: "Pleroma",
source_url: "https://git.pleroma.social/pleroma/pleroma",
source_url_pattern:
"https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}",
homepage_url: "https://pleroma.social/",
docs: [
logo: "priv/static/static/logo.png",
extras: ["README.md", "config/config.md"],
main: "readme"
]
]
end

@@ -48,11 +60,14 @@ defmodule Pleroma.Mixfile do
{:mogrify, "~> 0.6.1"},
{:ex_aws, "~> 2.0"},
{:ex_aws_s3, "~> 2.0"},
{:earmark, "~> 1.2"},
{:ex_machina, "~> 2.2", only: :test},
{:credo, "~> 0.9.3", only: [:dev, :test]},
{:mock, "~> 0.3.1", only: :test},
{:crypt,
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},
{:web_push_encryption, "~> 0.2.1"}
]
end
@@ -70,4 +85,51 @@ defmodule Pleroma.Mixfile do
test: ["ecto.create --quiet", "ecto.migrate", "test"]
]
end

# Builds a version string made of:
# * the application version
# * a pre-release if ahead of the tag: the describe string (-count-commithash)
# * build info:
# * a build name if `PLEROMA_BUILD_NAME` or `:pleroma, :build_name` is defined
# * the mix environment if different than prod
defp version(version) do
{git_tag, git_pre_release} =
with {tag, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=0"]),
tag = String.trim(tag),
{describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]),
describe = String.trim(describe),
ahead <- String.replace(describe, tag, "") do
{String.replace_prefix(tag, "v", ""), if(ahead != "", do: String.trim(ahead))}
else
_ -> {nil, nil}
end

if git_tag && version != git_tag do
Mix.shell().error(
"Application version #{inspect(version)} does not match git tag #{inspect(git_tag)}"
)
end

build_name =
cond do
name = Application.get_env(:pleroma, :build_name) -> name
name = System.get_env("PLEROMA_BUILD_NAME") -> name
true -> nil
end

env_name = if Mix.env() != :prod, do: to_string(Mix.env())

build =
[build_name, env_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join("-")
|> (fn
"" -> nil
string -> "+" <> string
end).()

[version, git_pre_release, build]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join()
end
end

+ 6
- 0
mix.lock View File

@@ -6,16 +6,19 @@
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
"credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]},
"db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"},
"ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.2.0", "fec496331e04fc2db2a1a24fe317c12c0c4a50d2beb8ebb3531ed1f0d84be0ed", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
@@ -24,6 +27,8 @@
"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"},
"jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
@@ -31,6 +36,7 @@
"mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},


+ 1
- 1
priv/static/index.html 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.f73fcf466fd253b0e5c6.js></script><script type=text/javascript src=/static/js/vendor.04ec6dccb5e616b6deac.js></script><script type=text/javascript src=/static/js/app.daf013e442326e175355.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.34667c2817916147413f.js></script><script type=text/javascript src=/static/js/vendor.32c621c7157f34c20923.js></script><script type=text/javascript src=/static/js/app.065638d22ade92dea420.js></script></body></html>

+ 23
- 0
priv/static/schemas/litepub-0.1.jsonld 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"
}
]
}

+ 4
- 1
priv/static/static/config.json View File

@@ -10,5 +10,8 @@
"showInstanceSpecificPanel": false,
"scopeOptionsEnabled": false,
"formattingOptionsEnabled": false,
"collapseMessageWithSubject": false
"collapseMessageWithSubject": false,
"hidePostStats": false,
"hideUserStats": false,
"loginMethod": "password"
}

+ 11
- 0
priv/static/static/js/app.065638d22ade92dea420.js
File diff suppressed because it is too large
View File


+ 1
- 0
priv/static/static/js/app.065638d22ade92dea420.js.map
File diff suppressed because it is too large
View File


+ 0
- 9
priv/static/static/js/app.daf013e442326e175355.js
File diff suppressed because it is too large
View File


+ 0
- 1
priv/static/static/js/app.daf013e442326e175355.js.map
File diff suppressed because it is too large
View File


priv/static/static/js/manifest.f73fcf466fd253b0e5c6.js → priv/static/static/js/manifest.34667c2817916147413f.js View File

@@ -1,2 +1,2 @@
!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:"04ec6dccb5e616b6deac",2:"daf013e442326e175355"}[e]+".js",n.appendChild(c)}},t.m=e,t.c=n,t.p="/"}([]);
//# sourceMappingURL=manifest.f73fcf466fd253b0e5c6.js.map
!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:"32c621c7157f34c20923",2:"065638d22ade92dea420"}[e]+".js",n.appendChild(c)}},t.m=e,t.c=n,t.p="/"}([]);
//# sourceMappingURL=manifest.34667c2817916147413f.js.map

priv/static/static/js/manifest.34667c2817916147413f.js.map
File diff suppressed because it is too large
View File


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

Loading…
Cancel
Save