Merge remote-tracking branch 'origin/develop' into shigusegubu

* origin/develop: (208 commits)
  Web.OAuth.OAuthControllerTest: Add test against token formatting
  Web.OAuth.OAuthController: Fix scopes Enum.join for OAuth response
  activitypub: fix date header format
  Added migration for setting default tags in existing users records
  config: fix chat endpoint path
  mastodon websocket: return errors using ok, not stop
  mastodon websocket: bring back infinity timeout
  mastodon websocket: use pattern match to get query data, robustly handle errors
  config: update config for cowboy 2 endpoints
  mastodon api: websocket: update code for cowboy 2.x
  mix: update dependencies for cowboy 2.0
  Renamed *DatabaseAuthenticator to *Authenticator.
  Added `auth_template/0` to DatabaseAuthenticator.
  Made auth customization be runtime-configurable.
  Added deactivated to the user view
  Remove parts of the old activity view.
  Add user muted status info to twitterapi.
  Add user muted status info to MastodonAPI.
  Add `with_muted` param.
  Follower requests: Utilize object index.
  ...
This commit is contained in:
Henry Jameson 2019-03-02 20:20:45 +02:00
commit ae07bb4779
563 changed files with 6342 additions and 2064 deletions

View file

@ -19,7 +19,7 @@
#
# You can give explicit globs or simply directories.
# In the latter case `**/*.{ex,exs}` will be used.
included: ["lib/", "src/", "web/", "apps/"],
included: ["lib/", "src/", "web/", "apps/", "test/"],
excluded: [~r"/_build/", ~r"/deps/"]
},
#
@ -57,7 +57,7 @@
# For some checks, like AliasUsage, you can only customize the priority
# Priority values are: `low, normal, high, higher`
{Credo.Check.Design.AliasUsage, priority: :low},
{Credo.Check.Design.AliasUsage, priority: :low, if_called_more_often_than: 3},
# For others you can set parameters
@ -104,7 +104,8 @@
{Credo.Check.Warning.BoolOperationOnSameValues},
{Credo.Check.Warning.IExPry},
{Credo.Check.Warning.IoInspect},
{Credo.Check.Warning.LazyLogging},
# Got too much of them, not sure if relevant
{Credo.Check.Warning.LazyLogging, false},
{Credo.Check.Warning.OperationOnSameValues},
{Credo.Check.Warning.OperationWithConstantResult},
{Credo.Check.Warning.UnusedEnumOperation},

View file

@ -1,7 +1,8 @@
image: elixir:1.7.2
services:
- postgres:9.6.2
- name: postgres:9.6.2
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
variables:
POSTGRES_DB: pleroma_test
@ -35,4 +36,4 @@ lint:
unit-testing:
stage: test
script:
- mix test --trace
- mix test --trace --preload-modules

View file

@ -8,19 +8,7 @@ Pleroma is written in Elixir, high-performance and can run on small devices like
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).
Client applications that are committed to supporting Pleroma:
* Mastalab (Android, Streaming Ready)
* Tusky (Android, No Streaming)
* Twidere (Android, No Streaming)
* Mast (iOS)
* Amaroq (iOS)
Client applications that are known to work well:
* Tootdon (Android + iOS)
* Tootle (iOS)
* Whalebird (Windows + Mac + Linux)
- [Client Applications for Pleroma](docs/Clients.md)
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>.

View file

@ -93,10 +93,11 @@ config :pleroma, Pleroma.Web.Endpoint,
dispatch: [
{:_,
[
{"/api/v1/streaming", Elixir.Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/socket/websocket", Phoenix.Endpoint.CowboyWebSocket,
{nil, {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
{:_, Plug.Adapters.Cowboy.Handler, {Pleroma.Web.Endpoint, []}}
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
{Phoenix.Transports.WebSocket,
{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
]}
]
],
@ -160,7 +161,10 @@ config :pleroma, :instance,
mrf_transparency: true,
autofollowed_nicknames: [],
max_pinned_statuses: 1,
no_attachment_links: false
no_attachment_links: false,
welcome_user_nickname: nil,
welcome_message: nil,
max_report_comment_size: 1000
config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because
@ -226,8 +230,8 @@ config :pleroma, :mrf_rejectnonpublic,
allow_direct: false
config :pleroma, :mrf_hellthread,
delist_threshold: 5,
reject_threshold: 10
delist_threshold: 10,
reject_threshold: 20
config :pleroma, :mrf_simple,
media_removal: [],
@ -236,6 +240,11 @@ config :pleroma, :mrf_simple,
reject: [],
accept: []
config :pleroma, :mrf_keyword,
reject: [],
federated_timeline_removal: [],
replace: []
config :pleroma, :rich_media, enabled: true
config :pleroma, :media_proxy,
@ -323,14 +332,27 @@ config :pleroma, Pleroma.User,
"web"
]
config :pleroma, Pleroma.Web.Federator, max_jobs: 50
config :pleroma, Pleroma.Web.Federator.RetryQueue,
enabled: false,
max_jobs: 20,
initial_timeout: 30,
max_retries: 5
config :pleroma, Pleroma.Jobs,
federator_incoming: [max_jobs: 50],
federator_outgoing: [max_jobs: 50],
mailer: [max_jobs: 10]
config :auto_linker,
opts: [
scheme: true,
extra: true,
class: false,
strip_prefix: false,
new_window: false,
rel: false
]
# 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"

View file

@ -16,7 +16,8 @@ config :pleroma, Pleroma.Web.Endpoint,
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: []
watchers: [],
secure_cookie_flag: false
config :pleroma, Pleroma.Mailer, adapter: Swoosh.Adapters.Local

View file

@ -44,6 +44,8 @@ config :web_push_encryption, :vapid_details,
"BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4",
private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA"
config :pleroma, Pleroma.Jobs, testing: [max_jobs: 2]
try do
import_config "test.secret.exs"
rescue

View file

@ -1,37 +1,84 @@
# Admin API
Authentication is required and the user must be an admin.
## `/api/pleroma/admin/users`
### List users
- Method `GET`
- Response:
```JSON
[
{
"deactivated": bool,
"id": integer,
"nickname": string
},
...
]
```
## `/api/pleroma/admin/user`
### Remove a user
* Method `DELETE`
* Params:
* `nickname`
* Response: Users nickname
- Method `DELETE`
- Params:
- `nickname`
- Response: Users nickname
### Create a user
* Method: `POST`
* Params:
* `nickname`
* `email`
* `password`
* Response: Users nickname
- Method: `POST`
- Params:
- `nickname`
- `email`
- `password`
- Response: Users nickname
## `/api/pleroma/admin/users/:nickname/toggle_activation`
### Toggle user activation
- Method: `PATCH`
- Params:
- `nickname`
- Response: Users object
```JSON
{
"deactivated": bool,
"id": integer,
"nickname": string
}
```
## `/api/pleroma/admin/users/tag`
### Tag a list of users
* Method: `PUT`
* Params:
* `nickname`
* `tags`
- Method: `PUT`
- Params:
- `nickname`
- `tags`
### Untag a list of users
* Method: `DELETE`
* Params:
* `nickname`
* `tags`
- Method: `DELETE`
- Params:
- `nickname`
- `tags`
## `/api/pleroma/admin/permission_group/:nickname`
### Get user user permission groups membership
* Method: `GET`
* Params: none
* Response:
- Method: `GET`
- Params: none
- Response:
```JSON
{
"is_moderator": bool,
@ -40,61 +87,87 @@ Authentication is required and the user must be an admin.
```
## `/api/pleroma/admin/permission_group/:nickname/:permission_group`
Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesnt exist.
### Get user user permission groups membership
* Method: `GET`
* Params: none
* Response:
- Method: `GET`
- Params: none
- Response:
```JSON
{
"is_moderator": bool,
"is_admin": bool
}
```
### Add user in permission group
* Method: `POST`
* Params: none
* Response:
* On failure: ``{"error": "…"}``
* On success: JSON of the ``user.info``
- Method: `POST`
- Params: none
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the `user.info`
### Remove user from permission group
* Method: `DELETE`
* Params: none
* Response:
* On failure: ``{"error": "…"}``
* On success: JSON of the ``user.info``
* Note: An admin cannot revoke their own admin status.
- Method: `DELETE`
- Params: none
- Response:
- On failure: `{"error": "…"}`
- On success: JSON of the `user.info`
- Note: An admin cannot revoke their own admin status.
## `/api/pleroma/admin/activation_status/:nickname`
### Active or deactivate a user
- Method: `PUT`
- Params:
- `nickname`
- `status` BOOLEAN field, false value means deactivation.
## `/api/pleroma/admin/relay`
### Follow a Relay
* Methods: `POST`
* Params:
* `relay_url`
* Response:
* On success: URL of the followed relay
- Methods: `POST`
- Params:
- `relay_url`
- Response:
- On success: URL of the followed relay
### Unfollow a Relay
* Methods: `DELETE`
* Params:
* `relay_url`
* Response:
* On success: URL of the unfollowed relay
- Methods: `DELETE`
- Params:
- `relay_url`
- Response:
- On success: URL of the unfollowed relay
## `/api/pleroma/admin/invite_token`
### Get a account registeration invite token
* Methods: `GET`
* Params: none
* Response: invite token (base64 string)
- Methods: `GET`
- Params: none
- Response: invite token (base64 string)
## `/api/pleroma/admin/email_invite`
### Sends registration invite via email
* Methods: `POST`
* Params:
* `email`
* `name`, optionnal
- Methods: `POST`
- Params:
- `email`
- `name`, optionnal
## `/api/pleroma/admin/password_reset`
### Get a password reset token for a given nickname
* Methods: `GET`
* Params: none
* Response: password reset token (base64 string)
- Methods: `GET`
- Params: none
- Response: password reset token (base64 string)

100
docs/Clients.md Normal file
View file

@ -0,0 +1,100 @@
# Pleroma Clients
Note: Additionnal clients may be working but theses are officially supporting Pleroma.
Feel free to contact us to be added to this list!
## Desktop
### Roma for Desktop
- Homepage: <http://www.pleroma.com/desktop-app/>
- Source Code: ???
- Platforms: Windows, Mac, (Linux?)
- Features: Streaming Ready
### Social
- Source Code: <https://gitlab.gnome.org/BrainBlasted/Social>
- Contact: [@brainblasted@social.libre.fi](https://social.libre.fi/users/brainblasted)
- Platforms: Linux (GNOME)
- Note(2019-01-28): Not at a pre-alpha stage yet
### Whalebird
- Homepage: <https://whalebird.org/>
- Source Code: <https://github.com/h3poteto/whalebird-desktop>
- Contact: [@h3poteto@pleroma.io](https://pleroma.io/users/h3poteto)
- Platforms: Windows, Mac, Linux
- Features: Streaming Ready
## Handheld
### Amaroq
- Homepage: <https://itunes.apple.com/us/app/amaroq-for-mastodon/id1214116200>
- Source Code: <https://github.com/ReticentJohn/Amaroq>
- Contact: [@eurasierboy@mastodon.social](https://mastodon.social/users/eurasierboy)
- Platforms: iOS
- Features: No Streaming
### Nekonium
- Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/)
- Source: <https://git.gdgd.jp.net/lin/nekonium/>
- Contact: [@lin@pleroma.gdgd.jp.net](https://pleroma.gdgd.jp.net/users/lin)
- Platforms: Android
- Features: Streaming Ready
### Mastalab
- Source Code: <https://gitlab.com/tom79/mastalab/>
- Contact: [@tom79@mastodon.social](https://mastodon.social/users/tom79)
- Platforms: Android
- Features: Streaming Ready
### Roma
- Homepage: <http://www.pleroma.com/>
- Source Code: ???
- Platforms: iOS, Android
- Features: No Streaming
### Tootdon
- Homepage: <http://tootdon.club/>, <http://blog.mastodon-tootdon.com/>
- Source Code: ???
- Contact: [@tootdon@mstdn.jp](https://mstdn.jp/users/tootdon)
- Platforms: Android, iOS
- Features: No Streaming
### Tusky
- Homepage: <https://tuskyapp.github.io/>
- Source Code: <https://github.com/tuskyapp/Tusky>
- Contact: [@ConnyDuck@mastodon.social](https://mastodon.social/users/ConnyDuck)
- Platforms: Android
- Features: No Streaming
### Twidere
- Homepage: <https://twidere.mariotaku.org/>
- Source Code: <https://github.com/TwidereProject/Twidere-Android/>, <https://github.com/TwidereProject/Twidere-iOS/>
- Contact: <me@mariotaku.org>
- Platform: Android, iOS
- Features: No Streaming
## Alternative Web Interfaces
### Brutaldon
- Homepage: <https://jfm.carcosa.net/projects/software/brutaldon/>
- Source Code: <https://github.com/jfmcbrayer/brutaldon>
- Contact: [@gcupc@glitch.social](https://glitch.social/users/gcupc)
- Features: No Streaming
### Feather
- Source Code: <https://github.com/kaniini/feather>
- Contact: [@kaniini@pleroma.site](https://pleroma.site/kaniini)
- Features: No Streaming
### Halcyon
- Source Code: <https://notabug.org/halcyon-suite/halcyon>
- Contact: [@halcyon@social.csswg.org](https://social.csswg.org/users/halcyon)
- Features: Streaming Ready
### Pinafore
- Homepage: <https://pinafore.social/>
- Source Code: <https://github.com/nolanlawson/pinafore>
- Contact: [@pinafore@mastodon.technology](https://mastodon.technology/users/pinafore)
- Note: Pleroma support is a secondary goal
- Features: No Streaming
### Sengi
- Source Code: <https://github.com/NicolasConstant/sengi>
- Contact: [@sengi_app@mastodon.social](https://mastodon.social/users/sengi_app)
- Note(2019-01-28): The development is currently in a early stage.

View file

@ -0,0 +1,15 @@
# Differences in Mastodon API responses from vanilla Mastodon
A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma <version>)" present in `version` field in response from `/api/v1/instance`
## Flake IDs
Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mastodon's ids they are sortable strings
## Attachment cap
Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.
## Timelines
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.

View file

@ -94,3 +94,17 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
## `/api/pleroma/admin/`
See [Admin-API](Admin-API.md)
## `/api/v1/pleroma/flavour/:flavour`
* Method `POST`
* Authentication: required
* Response: JSON string. Returns the user flavour or the default one on success, otherwise returns `{"error": "error_msg"}`
* Example response: "glitch"
* Note: This is intended to be used only by mastofe
## `/api/v1/pleroma/flavour`
* Method `GET`
* Authentication: required
* Response: JSON string. Returns the user flavour or the default one.
* Example response: "glitch"
* Note: This is intended to be used only by mastofe

View file

@ -36,14 +36,15 @@ This filter replaces the filename (not the path) of an upload. For complete obfu
An example for Sendgrid adapter:
```
```exs
config :pleroma, Pleroma.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: "YOUR_API_KEY"
```
An example for SMTP adapter:
```
```exs
config :pleroma, Pleroma.Mailer,
adapter: Swoosh.Adapters.SMTP,
relay: "smtp.gmail.com",
@ -97,6 +98,9 @@ config :pleroma, Pleroma.Mailer,
* `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature.
* `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow.
* `no_attachment_links`: Set to true to disable automatically adding attachment link text to statuses
* `welcome_message`: A message that will be send to a newly registered users as a direct message.
* `welcome_user_nickname`: The nickname of the local user that sends the welcome message.
* `max_report_size`: The maximum size of the report comment (Default: `1000`)
## :logger
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog
@ -171,6 +175,11 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
* `delist_threshold`: Number of mentioned users after which the message gets delisted (the message can still be seen, but it will not show up in public timelines and mentioned users won't get notifications about it). Set to 0 to disable.
* `reject_threshold`: Number of mentioned users after which the messaged gets rejected. Set to 0 to disable.
## :mrf_keyword
* `reject`: A list of patterns which result in message being rejected, each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html)
* `federated_timeline_removal`: A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html)
* `replace`: A list of tuples containing `{pattern, replacement}`, `pattern` can be a string or a [regular expression](https://hexdocs.pm/elixir/Regex.html)
## :media_proxy
* `enabled`: Enables proxying of remote media to the instances proxy
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
@ -202,7 +211,7 @@ their ActivityPub ID.
An example:
```
```exs
config :pleroma, :mrf_user_allowlist,
"example.org": ["https://example.org/users/admin"]
```
@ -231,18 +240,34 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end
Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the 'admin_token' parameter. Example:
```
```exs
config :pleroma, :admin_token, "somerandomtoken"
```
You can then do
```
```sh
curl "http://localhost:4000/api/pleroma/admin/invite_token?admin_token=somerandomtoken"
```
## Pleroma.Web.Federator
## Pleroma.Jobs
A list of job queues and their settings.
Job queue settings:
* `max_jobs`: The maximum amount of parallel jobs running at the same time.
Example:
```exs
config :pleroma, Pleroma.Jobs,
federator_incoming: [max_jobs: 50],
federator_outgoing: [max_jobs: 50]
```
This config contains two queues: `federator_incoming` and `federator_outgoing`. Both have the `max_jobs` set to `50`.
* `max_jobs`: The maximum amount of parallel federation jobs running at the same time.
## Pleroma.Web.Federator.RetryQueue
@ -276,3 +301,28 @@ For each pool, the options are:
* `max_connections` - how much connections a pool can hold
* `timeout` - retention duration for connections
## :auto_linker
Configuration for the `auto_linker` library:
* `class: "auto-linker"` - specify the class to be added to the generated link. false to clear
* `rel: "noopener noreferrer"` - override the rel attribute. false to clear
* `new_window: true` - set to false to remove `target='_blank'` attribute
* `scheme: false` - Set to true to link urls with schema `http://google.com`
* `truncate: false` - Set to a number to truncate urls longer then the number. Truncated urls will end in `..`
* `strip_prefix: true` - Strip the scheme prefix
* `extra: false` - link urls with rarely used schemes (magnet, ipfs, irc, etc.)
Example:
```exs
config :auto_linker,
opts: [
scheme: true,
extra: true,
class: false,
strip_prefix: false,
new_window: false,
rel: false
]
```

View file

@ -23,6 +23,11 @@ example.tld {
# 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 {
match_path /media
default_max_age 720m
}
cache {
match_path /proxy
default_max_age 720m

View file

@ -1,7 +1,7 @@
#!/sbin/openrc-run
# Requires OpenRC >= 0.35
directory=~pleroma/pleroma
directory=/opt/pleroma
command=/usr/bin/mix
command_args="phx.server"

View file

@ -1,6 +1,7 @@
# default Apache site config for Pleroma
#
# needed modules: define headers proxy proxy_http proxy_wstunnel rewrite ssl
# optional modules: cache cache_disk
#
# Simple installation instructions:
# 1. Install your TLS certificate, possibly using Let's Encrypt.
@ -8,6 +9,14 @@
# 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.
#
# Optional: enable disk-based caching for the media proxy
# For details, see https://git.pleroma.social/pleroma/pleroma/wikis/How%20to%20activate%20mediaproxy
#
# 1. Create the directory listed below as the CacheRoot, and make sure
# the Apache user can write to it.
# 2. Configure Apache's htcacheclean to clean the directory periodically.
# 3. Run 'a2enmod cache cache_disk' and restart Apache.
Define servername example.tld
@ -34,6 +43,15 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLCompression off
SSLSessionTickets off
# uncomment the following to enable mediaproxy caching on disk
# <IfModule mod_cache_disk.c>
# CacheRoot /var/cache/apache2/mod_cache_disk
# CacheDirLevels 1
# CacheDirLength 2
# CacheEnable disk /proxy
# CacheLock on
# </IfModule>
RewriteEngine On
RewriteCond %{HTTP:Connection} Upgrade [NC]
RewriteCond %{HTTP:Upgrade} websocket [NC]

View file

@ -11,16 +11,19 @@ proxy_cache_path /tmp/pleroma-media-cache levels=1:2 keys_zone=pleroma_media_cac
server {
server_name example.tld;
listen 80;
listen [::]:80;
return 301 https://$server_name$request_uri;
# Uncomment this if you need to use the 'webroot' method with certbot. Make sure
# that you also create the .well-known/acme-challenge directory structure in pleroma/priv/static and
# that is is accessible by the webserver. You may need to load this file with the ssl
# server block commented out, run certbot to get the certificate, and then uncomment it.
# that the directory exists and that it is accessible by the webserver. If you followed
# the guide, you already ran 'sudo mkdir -p /var/lib/letsencrypt' to create the folder.
# You may need to load this file with the ssl server block commented out, run certbot
# to get the certificate, and then uncomment it.
#
# location ~ /\.well-known/acme-challenge {
# root <path to install>/pleroma/priv/static/;
# root /var/lib/letsencrypt/.well-known/acme-challenge;
# }
}
@ -28,7 +31,10 @@ server {
ssl_session_cache shared:ssl_session_cache:10m;
server {
server_name example.tld;
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_session_timeout 5m;
ssl_trusted_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
@ -47,8 +53,6 @@ server {
ssl_stapling on;
ssl_stapling_verify on;
server_name example.tld;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;

View file

@ -15,15 +15,17 @@ Environment="MIX_ENV=prod"
; Make sure that all paths fit your installation.
; Path to the home directory of the user running the Pleroma service.
Environment="HOME=/home/pleroma"
Environment="HOME=/var/lib/pleroma"
; Path to the folder containing the Pleroma installation.
WorkingDirectory=/home/pleroma/pleroma
WorkingDirectory=/opt/pleroma
; Path to the Mix binary.
ExecStart=/usr/bin/mix phx.server
; 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
; The /home, /root, and /run/user folders can not be accessed by this service anymore. If your Pleroma user has its home folder in one of the restricted places, or use one of these folders as its working directory, you have to set this to false.
ProtectHome=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.

View file

@ -4,7 +4,8 @@
defmodule Mix.Tasks.Pleroma.Uploads do
use Mix.Task
alias Pleroma.{Upload, Uploaders.Local}
alias Pleroma.Upload
alias Pleroma.Uploaders.Local
alias Mix.Tasks.Pleroma.Common
require Logger
@ -20,7 +21,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do
- `--delete` - delete local uploads after migrating them to the target uploader
A list of avalible uploaders can be seen in config.exs
A list of available uploaders can be seen in config.exs
"""
def run(["migrate_local", target_uploader | args]) do
delete? = Enum.member?(args, "--delete")
@ -96,6 +97,7 @@ defmodule Mix.Tasks.Pleroma.Uploads do
timeout: 150_000
)
|> Stream.chunk_every(@log_every)
# credo:disable-for-next-line Credo.Check.Warning.UnusedEnumOperation
|> Enum.reduce(0, fn done, count ->
count = count + length(done)
Mix.shell().info("Uploaded #{count}/#{total_count} files")

View file

@ -5,7 +5,8 @@
defmodule Mix.Tasks.Pleroma.User do
use Mix.Task
import Ecto.Changeset
alias Pleroma.{Repo, User}
alias Pleroma.Repo
alias Pleroma.User
alias Mix.Tasks.Pleroma.Common
@shortdoc "Manages Pleroma users"
@ -211,7 +212,7 @@ defmodule Mix.Tasks.Pleroma.User do
user = Repo.get(User, user.id)
if length(user.following) == 0 do
if Enum.empty?(user.following) do
Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}")
end
else

View file

@ -7,7 +7,9 @@ defmodule Pleroma.PasswordResetToken do
import Ecto.Changeset
alias Pleroma.{User, PasswordResetToken, Repo}
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.PasswordResetToken
schema "password_reset_tokens" do
belongs_to(:user, User, type: Pleroma.FlakeId)

View file

@ -4,7 +4,11 @@
defmodule Pleroma.Activity do
use Ecto.Schema
alias Pleroma.{Repo, Activity, Notification}
alias Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.Notification
import Ecto.Query
@type t :: %__MODULE__{}
@ -109,4 +113,14 @@ defmodule Pleroma.Activity do
end
def mastodon_notification_type(%Activity{}), do: nil
def all_by_actor_and_id(actor, status_ids \\ [])
def all_by_actor_and_id(_actor, []), do: []
def all_by_actor_and_id(actor, status_ids) do
Activity
|> where([s], s.id in ^status_ids)
|> where([s], s.actor == ^actor)
|> Repo.all()
end
end

View file

@ -108,9 +108,10 @@ defmodule Pleroma.Application do
hackney_pool_children() ++
[
worker(Pleroma.Web.Federator.RetryQueue, []),
worker(Pleroma.Web.Federator, []),
worker(Pleroma.Stats, []),
worker(Pleroma.Web.Push, [])
worker(Pleroma.Web.Push, []),
worker(Pleroma.Jobs, []),
worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary)
] ++
streamer_child() ++
chat_child() ++

View file

@ -3,9 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha do
alias Calendar.DateTime
alias Plug.Crypto.KeyGenerator
alias Plug.Crypto.MessageEncryptor
alias Calendar.DateTime
use GenServer

View file

@ -0,0 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.AdminEmail do
@moduledoc "Admin emails"
import Swoosh.Email
alias Pleroma.Web.Router.Helpers
defp instance_config, do: Pleroma.Config.get(:instance)
defp instance_name, do: instance_config()[:name]
defp instance_email, do: instance_config()[:email]
defp user_url(user) do
Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname)
end
def report(to, reporter, account, statuses, comment) do
comment_html =
if comment do
"<p>Comment: #{comment}"
else
""
end
statuses_html =
if length(statuses) > 0 do
statuses_list_html =
statuses
|> Enum.map(fn %{id: id} ->
status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, id)
"<li><a href=\"#{status_url}\">#{status_url}</li>"
end)
|> Enum.join("\n")
"""
<p> Statuses:
<ul>
#{statuses_list_html}
</ul>
</p>
"""
else
""
end
html_body = """
<p>Reported by: <a href="#{user_url(reporter)}">#{reporter.nickname}</a></p>
<p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p>
#{comment_html}
#{statuses_html}
"""
new()
|> to({to.name, to.email})
|> from({instance_name(), instance_email()})
|> reply_to({reporter.name, reporter.email})
|> subject("#{instance_name()} Report")
|> html_body(html_body)
end
end

View file

@ -4,4 +4,10 @@
defmodule Pleroma.Mailer do
use Swoosh.Mailer, otp_app: :pleroma
def deliver_async(email, config \\ []) do
Pleroma.Jobs.enqueue(:mailer, __MODULE__, [:deliver_async, email, config])
end
def perform(:deliver_async, email, config), do: deliver(email, config)
end

View file

@ -7,7 +7,8 @@ defmodule Pleroma.UserEmail do
import Swoosh.Email
alias Pleroma.Web.{Endpoint, Router}
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router
defp instance_config, do: Pleroma.Config.get(:instance)

View file

@ -4,8 +4,12 @@
defmodule Pleroma.Filter do
use Ecto.Schema
import Ecto.{Changeset, Query}
alias Pleroma.{User, Repo}
import Ecto.Changeset
import Ecto.Query
alias Pleroma.User
alias Pleroma.Repo
schema "filters" do
belongs_to(:user, User, type: Pleroma.FlakeId)

View file

@ -27,7 +27,7 @@ defmodule Pleroma.FlakeId do
Kernel.to_string(id)
end
def to_string(flake = <<_::integer-size(64), _::integer-size(48), _::integer-size(16)>>) do
def to_string(<<_::integer-size(64), _::integer-size(48), _::integer-size(16)>> = flake) do
encode_base62(flake)
end
@ -42,7 +42,7 @@ defmodule Pleroma.FlakeId do
def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>>
end
def from_string(flake = <<_::integer-size(128)>>), do: flake
def from_string(<<_::integer-size(128)>> = flake), do: flake
def from_string(string) when is_binary(string) and byte_size(string) < 18 do
case Integer.parse(string) do

View file

@ -3,38 +3,56 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Formatter do
alias Pleroma.Emoji
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.MediaProxy
alias Pleroma.HTML
alias Pleroma.Emoji
@tag_regex ~r/((?<=[^&])|\A)(\#)(\w+)/u
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
@link_regex ~r{((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+}ui
# Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
@mentions_regex ~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9_-](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u
@auto_linker_config hashtag: true,
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
mention: true,
mention_handler: &Pleroma.Formatter.mention_handler/4
def parse_tags(text, data \\ %{}) do
Regex.scan(@tag_regex, text)
|> Enum.map(fn ["#" <> tag = full_tag | _] -> {full_tag, String.downcase(tag)} end)
|> (fn map ->
if data["sensitive"] in [true, "True", "true", "1"],
do: [{"#nsfw", "nsfw"}] ++ map,
else: map
end).()
def mention_handler("@" <> nickname, buffer, opts, acc) do
case User.get_cached_by_nickname(nickname) do
%User{id: id} = user ->
ap_id = get_ap_id(user)
nickname_text = get_nickname_text(nickname, opts) |> maybe_escape(opts)
link =
"<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>@<span>#{
nickname_text
}</span></a></span>"
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
_ ->
{buffer, acc}
end
end
@doc "Parses mentions text and returns list {nickname, user}."
@spec parse_mentions(binary()) :: list({binary(), User.t()})
def parse_mentions(text) do
Regex.scan(@mentions_regex, text)
|> List.flatten()
|> Enum.uniq()
|> Enum.map(fn nickname ->
with nickname <- String.trim_leading(nickname, "@"),
do: {"@" <> nickname, User.get_cached_by_nickname(nickname)}
end)
|> Enum.filter(fn {_match, user} -> user end)
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag)
url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
link = "<a class='hashtag' data-tag='#{tag}' href='#{url}' rel='tag'>#{tag_text}</a>"
{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
end
@doc """
Parses a text and replace plain text links with HTML. Returns a tuple with a result text, mentions, and hashtags.
"""
@spec linkify(String.t(), keyword()) ::
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
def linkify(text, options \\ []) do
options = options ++ @auto_linker_config
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
end
def emojify(text) do
@ -48,9 +66,7 @@ defmodule Pleroma.Formatter do
emoji = HTML.strip_tags(emoji)
file = HTML.strip_tags(file)
String.replace(
text,
":#{emoji}:",
html =
if not strip do
"<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
MediaProxy.url(file)
@ -58,8 +74,8 @@ defmodule Pleroma.Formatter do
else
""
end
)
|> HTML.filter_tags()
String.replace(text, ":#{emoji}:", html) |> HTML.filter_tags()
end)
end
@ -75,12 +91,10 @@ defmodule Pleroma.Formatter do
def get_emoji(_), do: []
@link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui
def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end
@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, "text/html") do
HTML.filter_tags(text)
end
@ -94,112 +108,6 @@ defmodule Pleroma.Formatter do
|> Enum.join("")
end
@doc """
Escapes a special characters in mention names.
"""
@spec mentions_escape(String.t(), list({String.t(), any()})) :: String.t()
def mentions_escape(text, mentions) do
mentions
|> Enum.reduce(text, fn {name, _}, acc ->
escape_name = String.replace(name, @markdown_characters_regex, "\\\\\\1")
String.replace(acc, name, escape_name)
end)
end
@doc "changes scheme:... urls to html links"
def add_links({subs, text}) do
links =
text
|> String.split([" ", "\t", "<br>"])
|> 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)
uuid_text =
links
|> Enum.reduce(text, fn {uuid, url}, acc -> String.replace(acc, url, uuid) end)
subs =
subs ++
Enum.map(links, fn {uuid, url} ->
{uuid, "<a href=\"#{url}\">#{url}</a>"}
end)
{subs, uuid_text}
end
@doc "Adds the links to mentioned users"
def add_user_links({subs, text}, mentions, options \\ []) do
mentions =
mentions
|> Enum.sort_by(fn {name, _} -> -String.length(name) end)
|> Enum.map(fn {name, user} -> {name, user, Ecto.UUID.generate()} end)
uuid_text =
mentions
|> Enum.reduce(text, fn {match, _user, uuid}, text ->
String.replace(text, match, uuid)
end)
subs =
subs ++
Enum.map(mentions, fn {match, %User{id: id, ap_id: ap_id, info: info}, uuid} ->
ap_id =
if is_binary(info.source_data["url"]) do
info.source_data["url"]
else
ap_id
end
nickname =
if options[:format] == :full do
User.full_nickname(match)
else
User.local_nickname(match)
end
{uuid,
"<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>" <>
"@<span>#{nickname}</span></a></span>"}
end)
{subs, uuid_text}
end
@doc "Adds the hashtag links"
def add_hashtag_links({subs, text}, tags) do
tags =
tags
|> Enum.sort_by(fn {name, _} -> -String.length(name) end)
|> Enum.map(fn {name, short} -> {name, short, Ecto.UUID.generate()} end)
uuid_text =
tags
|> Enum.reduce(text, fn {match, _short, uuid}, text ->
String.replace(text, ~r/((?<=[^&])|(\A))#{match}/, uuid)
end)
subs =
subs ++
Enum.map(tags, fn {tag_text, tag, uuid} ->
url =
"<a class='hashtag' data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{
tag_text
}</a>"
{uuid, url}
end)
{subs, uuid_text}
end
def finalize({subs, text}) do
Enum.reduce(subs, text, fn {uuid, replacement}, result_text ->
String.replace(result_text, uuid, replacement)
end)
end
def truncate(text, max_length \\ 200, omission \\ "...") do
# Remove trailing whitespace
text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}")
@ -211,4 +119,16 @@ defmodule Pleroma.Formatter do
String.slice(text, 0, length_with_omission) <> omission
end
end
defp get_ap_id(%User{info: %{source_data: %{"url" => url}}}) when is_binary(url), do: url
defp get_ap_id(%User{ap_id: ap_id}), do: ap_id
defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)
defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)
defp maybe_escape(str, %{mentions_escape: true}) do
String.replace(str, @markdown_characters_regex, "\\\\\\1")
end
defp maybe_escape(str, _), do: str
end

View file

@ -37,17 +37,18 @@ end
defmodule Pleroma.Gopher.Server.ProtocolHandler do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Repo
def start_link(ref, socket, transport, opts) do
pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts])
{:ok, pid}
end
def init(ref, socket, transport, _Opts = []) do
def init(ref, socket, transport, [] = _Opts) do
:ok = :ranch.accept_ack(ref)
loop(socket, transport)
end
@ -110,7 +111,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
def response("/notices/" <> id) do
with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.is_public?(activity) do
true <- Visibility.is_public?(activity) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"])
|> render_activities

View file

@ -83,8 +83,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
"""
@markup Application.get_env(:pleroma, :markup)
@uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
@ -126,10 +125,11 @@ defmodule Pleroma.HTML.Scrubber.Default do
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
# credo:disable-for-previous-line
# No idea how to fix this one…
@markup Application.get_env(:pleroma, :markup)
@uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()

View file

@ -2,13 +2,13 @@ defmodule Pleroma.Instances.Instance do
@moduledoc "Instance."
alias Pleroma.Instances
alias Pleroma.Repo
alias Pleroma.Instances.Instance
use Ecto.Schema
import Ecto.{Query, Changeset}
alias Pleroma.Repo
import Ecto.Query
import Ecto.Changeset
schema "instances" do
field(:host, :string)

152
lib/pleroma/jobs.ex Normal file
View file

@ -0,0 +1,152 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Jobs do
@moduledoc """
A basic job queue
"""
use GenServer
require Logger
def init(args) do
{:ok, args}
end
def start_link do
queues =
Pleroma.Config.get(Pleroma.Jobs)
|> Enum.map(fn {name, _} -> create_queue(name) end)
|> Enum.into(%{})
state = %{
queues: queues,
refs: %{}
}
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def create_queue(name) do
{name, {:sets.new(), []}}
end
@doc """
Enqueues a job.
Returns `:ok`.
## Arguments
- `queue_name` - a queue name(must be specified in the config).
- `mod` - a worker module (must have `perform` function).
- `args` - a list of arguments for the `perform` function of the worker module.
- `priority` - a job priority (`0` by default).
## Examples
Enqueue `Module.perform/0` with `priority=1`:
iex> Pleroma.Jobs.enqueue(:example_queue, Module, [])
:ok
Enqueue `Module.perform(:job_name)` with `priority=5`:
iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:job_name], 5)
:ok
Enqueue `Module.perform(:another_job, data)` with `priority=1`:
iex> data = "foobar"
iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:another_job, data])
:ok
Enqueue `Module.perform(:foobar_job, :foo, :bar, 42)` with `priority=1`:
iex> Pleroma.Jobs.enqueue(:example_queue, Module, [:foobar_job, :foo, :bar, 42])
:ok
"""
def enqueue(queue_name, mod, args, priority \\ 1)
if Mix.env() == :test do
def enqueue(_queue_name, mod, args, _priority) do
apply(mod, :perform, args)
end
else
@spec enqueue(atom(), atom(), [any()], integer()) :: :ok
def enqueue(queue_name, mod, args, priority) do
GenServer.cast(__MODULE__, {:enqueue, queue_name, mod, args, priority})
end
end
def handle_cast({:enqueue, queue_name, mod, args, priority}, state) do
{running_jobs, queue} = state[:queues][queue_name]
queue = enqueue_sorted(queue, {mod, args}, priority)
state =
state
|> update_queue(queue_name, {running_jobs, queue})
|> maybe_start_job(queue_name, running_jobs, queue)
{:noreply, state}
end
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
queue_name = state.refs[ref]
{running_jobs, queue} = state[:queues][queue_name]
running_jobs = :sets.del_element(ref, running_jobs)
state =
state
|> remove_ref(ref)
|> update_queue(queue_name, {running_jobs, queue})
|> maybe_start_job(queue_name, running_jobs, queue)
{:noreply, state}
end
def maybe_start_job(state, queue_name, running_jobs, queue) do
if :sets.size(running_jobs) < Pleroma.Config.get([__MODULE__, queue_name, :max_jobs]) &&
queue != [] do
{{mod, args}, queue} = queue_pop(queue)
{:ok, pid} = Task.start(fn -> apply(mod, :perform, args) end)
mref = Process.monitor(pid)
state
|> add_ref(queue_name, mref)
|> update_queue(queue_name, {:sets.add_element(mref, running_jobs), queue})
else
state
end
end
def enqueue_sorted(queue, element, priority) do
[%{item: element, priority: priority} | queue]
|> Enum.sort_by(fn %{priority: priority} -> priority end)
end
def queue_pop([%{item: element} | queue]) do
{element, queue}
end
defp add_ref(state, queue_name, ref) do
refs = Map.put(state[:refs], ref, queue_name)
Map.put(state, :refs, refs)
end
defp remove_ref(state, ref) do
refs = Map.delete(state[:refs], ref)
Map.put(state, :refs, refs)
end
defp update_queue(state, queue_name, data) do
queues = Map.put(state[:queues], queue_name, data)
Map.put(state, :queues, queues)
end
end

View file

@ -4,8 +4,13 @@
defmodule Pleroma.List do
use Ecto.Schema
import Ecto.{Changeset, Query}
alias Pleroma.{User, Repo, Activity}
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.User
schema "lists" do
belongs_to(:user, User, type: Pleroma.FlakeId)

View file

@ -4,8 +4,14 @@
defmodule Pleroma.Notification do
use Ecto.Schema
alias Pleroma.{User, Activity, Notification, Repo}
alias Pleroma.User
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.CommonAPI
import Ecto.Query
schema "notifications" do
@ -112,7 +118,7 @@ defmodule Pleroma.Notification do
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do
unless User.blocks?(user, %{ap_id: activity.data["actor"]}) or
user.ap_id == activity.data["actor"] or
CommonAPI.thread_muted?(user, activity) or user.ap_id == activity.data["actor"] or
(activity.data["type"] == "Follow" and
Enum.any?(Notification.for_user(user), fn notif ->
notif.activity.data["type"] == "Follow" and

View file

@ -4,8 +4,15 @@
defmodule Pleroma.Object do
use Ecto.Schema
alias Pleroma.{Repo, Object, User, Activity, ObjectTombstone}
import Ecto.{Query, Changeset}
alias Pleroma.Repo
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Activity
alias Pleroma.ObjectTombstone
import Ecto.Query
import Ecto.Changeset
schema "objects" do
field(:data, :map)

View file

@ -33,7 +33,22 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
end
defp csp_string do
protocol = Config.get([Pleroma.Web.Endpoint, :protocol])
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
websocket_url = String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
connect_src =
if Mix.env() == :dev do
"connect-src 'self' http://localhost:3035/ " <> websocket_url
else
"connect-src 'self' " <> websocket_url
end
script_src =
if Mix.env() == :dev do
"script-src 'self' 'unsafe-eval'"
else
"script-src 'self'"
end
[
"default-src 'none'",
@ -43,10 +58,10 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
"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
connect_src,
script_src,
if scheme == "https" do
"upgrade-insecure-requests"
end
]

View file

@ -33,7 +33,7 @@ defmodule Pleroma.Plugs.InstanceStatic do
for only <- @only do
at = Plug.Router.Utils.split("/")
def call(conn = %{request_path: "/" <> unquote(only) <> _}, opts) do
def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
call_static(
conn,
opts,

View file

@ -6,11 +6,9 @@ defmodule Pleroma.Plugs.OAuthPlug do
import Plug.Conn
import Ecto.Query
alias Pleroma.{
User,
Repo,
Web.OAuth.Token
}
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.Web.OAuth.Token
@realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")

View file

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.OAuthScopesPlug do
import Plug.Conn
@behaviour Plug
def init(%{scopes: _} = options), do: options
def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
op = options[:op] || :|
token = assigns[:token]
cond do
is_nil(token) ->
conn
op == :| && scopes -- token.scopes != scopes ->
conn
op == :& && scopes -- token.scopes == [] ->
conn
options[:fallback] == :proceed_unauthenticated ->
conn
|> assign(:user, nil)
|> assign(:token, nil)
true ->
missing_scopes = scopes -- token.scopes
error_message = "Insufficient permissions: #{Enum.join(missing_scopes, " #{op} ")}."
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{error: error_message}))
|> halt()
end
end
end

View file

@ -23,7 +23,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
%{static_plug_opts: static_plug_opts}
end
def call(conn = %{request_path: <<"/", @path, "/", file::binary>>}, opts) do
def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
config = Pleroma.Config.get([Pleroma.Upload])
with uploader <- Keyword.fetch!(config, :uploader),

View file

@ -3,9 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserFetcherPlug do
import Plug.Conn
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Repo
import Plug.Conn
def init(options) do
options

View file

@ -4,7 +4,8 @@
defmodule Pleroma.Stats do
import Ecto.Query
alias Pleroma.{User, Repo}
alias Pleroma.User
alias Pleroma.Repo
def start_link do
agent = Agent.start_link(fn -> {[], %{}} end, name: __MODULE__)
@ -23,7 +24,7 @@ defmodule Pleroma.Stats do
def schedule_update do
spawn(fn ->
# 1 hour
Process.sleep(1000 * 60 * 60 * 1)
Process.sleep(1000 * 60 * 60)
schedule_update()
end)

View file

@ -0,0 +1,45 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ThreadMute do
use Ecto.Schema
alias Pleroma.{Repo, User, ThreadMute}
require Ecto.Query
schema "thread_mutes" do
belongs_to(:user, User, type: Pleroma.FlakeId)
field(:context, :string)
end
def changeset(mute, params \\ %{}) do
mute
|> Ecto.Changeset.cast(params, [:user_id, :context])
|> Ecto.Changeset.foreign_key_constraint(:user_id)
|> Ecto.Changeset.unique_constraint(:user_id, name: :unique_index)
end
def query(user_id, context) do
user_id = Pleroma.FlakeId.from_string(user_id)
ThreadMute
|> Ecto.Query.where(user_id: ^user_id)
|> Ecto.Query.where(context: ^context)
end
def add_mute(user_id, context) do
%ThreadMute{}
|> changeset(%{user_id: user_id, context: context})
|> Repo.insert()
end
def remove_mute(user_id, context) do
query(user_id, context)
|> Repo.delete_all()
end
def check_muted(user_id, context) do
query(user_id, context)
|> Repo.all()
end
end

View file

@ -180,7 +180,7 @@ defmodule Pleroma.Upload do
end
# For Mix.Tasks.MigrateLocalUploads
defp prepare_upload(upload = %__MODULE__{tempfile: path}, _opts) do
defp prepare_upload(%__MODULE__{tempfile: path} = upload, _opts) do
with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do
{:ok, %__MODULE__{upload | content_type: content_type}}
end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Upload.Filter.Dedupe do
@behaviour Pleroma.Upload.Filter
alias Pleroma.Upload
def filter(upload = %Upload{name: name}) do
def filter(%Upload{name: name} = upload) do
extension = String.split(name, ".") |> List.last()
shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower)
filename = shasum <> "." <> extension

View file

@ -25,7 +25,7 @@ defmodule Pleroma.Uploaders.MDII do
query = "#{cgi}?#{extension}"
with {:ok, %{status: 200, body: body}} <-
@httpoison.post(query, file_data, adapter: [pool: :default]) do
@httpoison.post(query, file_data, [], adapter: [pool: :default]) do
remote_file_name = String.split(body) |> List.first()
public_url = "#{files}/#{remote_file_name}.#{extension}"
{:ok, {:url, public_url}}

View file

@ -27,7 +27,7 @@ defmodule Pleroma.Uploaders.S3 do
])}}
end
def put_file(upload = %Pleroma.Upload{}) do
def put_file(%Pleroma.Upload{} = upload) do
config = Pleroma.Config.get([__MODULE__])
bucket = Keyword.get(config, :bucket)

View file

@ -5,13 +5,23 @@
defmodule Pleroma.User do
use Ecto.Schema
import Ecto.{Changeset, Query}
alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Object
alias Pleroma.Web
alias Pleroma.Activity
alias Pleroma.Notification
alias Comeonin.Pbkdf2
alias Pleroma.Formatter
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.{OStatus, Websub, OAuth}
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
alias Pleroma.Web.OStatus
alias Pleroma.Web.Websub
alias Pleroma.Web.OAuth
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.ActivityPub
require Logger
@ -96,12 +106,6 @@ defmodule Pleroma.User do
"#{ap_id(user)}/followers"
end
def follow_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:following])
|> validate_required([:following])
end
def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0
@ -233,6 +237,7 @@ defmodule Pleroma.User do
changeset
|> put_change(:password_hash, hashed)
|> put_change(:ap_id, ap_id)
|> unique_constraint(:ap_id)
|> put_change(:following, [followers])
|> put_change(:follower_address, followers)
else
@ -256,8 +261,9 @@ defmodule Pleroma.User do
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset),
{:ok, _} <- try_send_confirmation_email(user),
{:ok, user} <- autofollow_users(user) do
{:ok, user} <- autofollow_users(user),
{:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
end
end
@ -267,7 +273,7 @@ defmodule Pleroma.User do
Pleroma.Config.get([:instance, :account_activation_required]) do
user
|> Pleroma.UserEmail.account_confirmation_email()
|> Pleroma.Mailer.deliver()
|> Pleroma.Mailer.deliver_async()
else
{:ok, :noop}
end
@ -307,10 +313,13 @@ defmodule Pleroma.User do
end
end
@doc "A mass follow for local users. Ignores blocks and has no side effects"
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do
followed_addresses = Enum.map(followeds, fn %{follower_address: fa} -> fa end)
followed_addresses =
followeds
|> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
|> Enum.map(fn %{follower_address: fa} -> fa end)
q =
from(u in User,
@ -604,13 +613,40 @@ defmodule Pleroma.User do
),
where:
fragment(
"? @> ?",
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
a.data,
^%{"object" => user.ap_id}
a.data,
^user.ap_id
)
)
end
def update_follow_request_count(%User{} = user) do
subquery =
user
|> User.get_follow_requests_query()
|> select([a], %{count: count(a.id)})
User
|> where(id: ^user.id)
|> join(:inner, [u], s in subquery(subquery))
|> update([u, s],
set: [
info:
fragment(
"jsonb_set(?, '{follow_request_count}', ?::varchar::jsonb, true)",
u.info,
s.count
)
]
)
|> Repo.update_all([], returning: true)
|> case do
{1, [user]} -> {:ok, user}
_ -> {:error, user}
end
end
def get_follow_requests(%User{} = user) do
q = get_follow_requests_query(user)
reqs = Repo.all(q)
@ -724,7 +760,7 @@ defmodule Pleroma.User do
# Strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
if resolve, do: User.get_or_fetch_by_nickname(query)
if resolve, do: get_or_fetch(query)
fts_results = do_search(fts_search_subquery(query), for_user)
@ -737,6 +773,12 @@ defmodule Pleroma.User do
Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
end
def all_except_one(user) do
query = from(u in User, where: u.id != ^user.id)
Repo.all(query)
end
defp do_search(subquery, for_user, options \\ []) do
q =
from(
@ -853,6 +895,30 @@ defmodule Pleroma.User do
)
end
def mute(muter, %User{ap_id: ap_id}) do
info_cng =
muter.info
|> User.Info.add_to_mutes(ap_id)
cng =
change(muter)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def unmute(muter, %{ap_id: ap_id}) do
info_cng =
muter.info
|> User.Info.remove_from_mutes(ap_id)
cng =
change(muter)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def block(blocker, %User{ap_id: ap_id} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
@ -895,6 +961,9 @@ defmodule Pleroma.User do
update_and_set_cache(cng)
end
def mutes?(nil, _), do: false
def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
def blocks?(user, %{ap_id: ap_id}) do
blocks = user.info.blocks
domain_blocks = user.info.domain_blocks
@ -906,6 +975,9 @@ defmodule Pleroma.User do
end)
end
def muted_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
def blocked_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
@ -1123,9 +1195,6 @@ defmodule Pleroma.User do
def parse_bio(bio, _user) when bio == "", do: bio
def parse_bio(bio, user) do
mentions = Formatter.parse_mentions(bio)
tags = Formatter.parse_tags(bio)
emoji =
(user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
@ -1134,7 +1203,8 @@ defmodule Pleroma.User do
end)
bio
|> CommonUtils.format_input(mentions, tags, "text/plain", user_links: [format: :full])
|> CommonUtils.format_input("text/plain", mentions_format: :full)
|> elem(0)
|> Formatter.emojify(emoji)
end
@ -1166,7 +1236,7 @@ defmodule Pleroma.User do
{:ok, updated_user} =
user
|> change(%{tags: new_tags})
|> Repo.update()
|> update_and_set_cache()
updated_user
end
@ -1220,4 +1290,13 @@ defmodule Pleroma.User do
inserted_at: NaiveDateTime.utc_now()
}
end
def all_superusers do
from(
u in User,
where: u.local == true,
where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
)
|> Repo.all()
end
end

View file

@ -12,12 +12,14 @@ defmodule Pleroma.User.Info do
field(:source_data, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:follow_request_count, :integer, default: 0)
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: [])
field(:domain_blocks, {:array, :string}, default: [])
field(:mutes, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
@ -34,6 +36,7 @@ defmodule Pleroma.User.Info do
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:pinned_activities, {:array, :string}, default: [])
field(:flavour, :string, default: nil)
# Found in the wild
# ap_id -> Where is this used?
@ -72,6 +75,14 @@ defmodule Pleroma.User.Info do
|> validate_required([:follower_count])
end
def set_mutes(info, mutes) do
params = %{mutes: mutes}
info
|> cast(params, [:mutes])
|> validate_required([:mutes])
end
def set_blocks(info, blocks) do
params = %{blocks: blocks}
@ -80,6 +91,14 @@ defmodule Pleroma.User.Info do
|> validate_required([:blocks])
end
def add_to_mutes(info, muted) do
set_mutes(info, Enum.uniq([muted | info.mutes]))
end
def remove_from_mutes(info, muted) do
set_mutes(info, List.delete(info.mutes, muted))
end
def add_to_block(info, blocked) do
set_blocks(info, Enum.uniq([blocked | info.blocks]))
end
@ -186,6 +205,14 @@ defmodule Pleroma.User.Info do
|> validate_required([:settings])
end
def mastodon_flavour_update(info, flavour) do
params = %{flavour: flavour}
info
|> cast(params, [:flavour])
|> validate_required([:flavour])
end
def set_source_data(info, source_data) do
params = %{source_data: source_data}

View file

@ -0,0 +1,30 @@
defmodule Pleroma.User.WelcomeMessage do
alias Pleroma.User
alias Pleroma.Web.CommonAPI
def post_welcome_message_to_user(user) do
with %User{} = sender_user <- welcome_user(),
message when is_binary(message) <- welcome_message() do
CommonAPI.post(sender_user, %{
"visibility" => "direct",
"status" => "@#{user.nickname}\n#{message}"
})
else
_ -> {:ok, nil}
end
end
defp welcome_user() do
with nickname when is_binary(nickname) <-
Pleroma.Config.get([:instance, :welcome_user_nickname]),
%User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user
else
_ -> nil
end
end
defp welcome_message() do
Pleroma.Config.get([:instance, :welcome_message])
end
end

View file

@ -3,13 +3,23 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.{Activity, Repo, Object, Upload, User, Notification, Instances}
alias Pleroma.Web.ActivityPub.{Transmogrifier, MRF}
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.Object
alias Pleroma.Upload
alias Pleroma.User
alias Pleroma.Notification
alias Pleroma.Instances
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Federator
alias Pleroma.Web.OStatus
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
import Pleroma.Web.ActivityPub.Visibility
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
@ -19,10 +29,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp get_recipients(%{"type" => "Announce"} = data) do
to = data["to"] || []
cc = data["cc"] || []
recipients = to ++ cc
actor = User.get_cached_by_ap_id(data["actor"])
recipients
recipients =
(to ++ cc)
|> Enum.filter(fn recipient ->
case User.get_cached_by_ap_id(recipient) do
nil ->
@ -119,7 +129,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
activity.data["object"]
|> Map.get("tag", [])
|> Enum.filter(fn tag -> is_bitstring(tag) end)
|> Enum.map(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
|> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
if activity.data["object"]["attachment"] != [] do
Pleroma.Web.Streamer.stream("public:media", activity)
@ -163,9 +173,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object},
with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
:ok <- maybe_federate(activity),
_ <- User.update_follow_request_count(actor) do
{:ok, activity}
end
end
@ -174,9 +185,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Reject", "actor" => actor, "object" => object},
with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
:ok <- maybe_federate(activity),
_ <- User.update_follow_request_count(actor) do
{:ok, activity}
end
end
@ -274,7 +286,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def follow(follower, followed, activity_id \\ nil, local \\ true) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
:ok <- maybe_federate(activity),
_ <- User.update_follow_request_count(followed) do
{:ok, activity}
end
end
@ -284,7 +297,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity) do
:ok <- maybe_federate(activity),
_ <- User.update_follow_request_count(followed) do
{:ok, activity}
end
end
@ -340,6 +354,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
def flag(
%{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content
} = params
) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
%{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content
}
|> make_flag_data(additional)
|> insert(local)
end
def fetch_activities_for_context(context, opts \\ %{}) do
public = ["https://www.w3.org/ns/activitystreams#Public"]
@ -563,6 +602,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_reblogs(query, _), do: query
defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
defp restrict_muted(query, %{"muting_user" => %User{info: info}}) do
mutes = info.mutes
from(
activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
)
end
defp restrict_muted(query, _), do: query
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info.blocks || []
domain_blocks = info.domain_blocks || []
@ -616,6 +669,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_type(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(opts)
|> restrict_muted(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_replies(opts)
@ -744,7 +798,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
public = is_public?(activity)
reachable_inboxes_metadata =
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{source_data: data}} ->
@ -753,12 +809,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
Enum.each(reachable_inboxes_metadata, fn {inbox, unreachable_since} ->
Federator.enqueue(:publish_single_ap, %{
|> Enum.each(fn {inbox, unreachable_since} ->
Federator.publish_single_ap(%{
inbox: inbox,
json: json,
actor: actor,
@ -774,11 +826,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date =
NaiveDateTime.utc_now()
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
signature =
Pleroma.Web.HTTPSignatures.sign(actor, %{
host: host,
"content-length": byte_size(json),
digest: digest
digest: digest,
date: date
})
with {:ok, %{status: code}} when code in 200..299 <-
@ -788,6 +845,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
json,
[
{"Content-Type", "application/activity+json"},
{"Date", date},
{"signature", signature},
{"digest", digest}
]
@ -809,8 +867,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if object = Object.get_cached_by_ap_id(id) do
{:ok, object}
else
Logger.info("Fetching #{id} via AP")
with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
nil <- Object.normalize(data),
params <- %{
@ -842,7 +898,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def fetch_and_contain_remote_object_from_id(id) do
Logger.info("Fetching #{id} via AP")
Logger.info("Fetching object #{id} via AP")
with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
@ -859,52 +915,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
def is_public?(%Object{data: data}), do: is_public?(data)
def is_public?(%Activity{data: data}), do: is_public?(data)
def is_public?(%{"directMessage" => true}), do: false
def is_public?(data) do
"https://www.w3.org/ns/activitystreams#Public" in (data["to"] ++ (data["cc"] || []))
end
def is_private?(activity) do
!is_public?(activity) && Enum.any?(activity.data["to"], &String.contains?(&1, "/followers"))
end
def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
def is_direct?(%Object{data: %{"directMessage" => true}}), do: true
def is_direct?(activity) do
!is_public?(activity) && !is_private?(activity)
end
def visible_for_user?(activity, nil) do
is_public?(activity)
end
def visible_for_user?(activity, user) do
x = [user.ap_id | user.following]
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)

View file

@ -5,12 +5,16 @@
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
use Pleroma.Web, :controller
alias Pleroma.{Activity, User, Object}
alias Pleroma.Web.ActivityPub.{ObjectView, UserView}
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator
require Logger
@ -46,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def object(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :object, uuid),
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, ActivityPub.is_public?(object)} do
{_, true} <- {:public?, Visibility.is_public?(object)} do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ObjectView.render("object.json", %{object: object}))
@ -59,7 +63,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
with ap_id <- o_status_url(conn, :object, uuid),
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, ActivityPub.is_public?(object)},
{_, true} <- {:public?, Visibility.is_public?(object)},
likes <- Utils.get_object_likes(object) do
{page, _} = Integer.parse(page)
@ -75,7 +79,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def object_likes(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :object, uuid),
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, ActivityPub.is_public?(object)},
{_, true} <- {:public?, Visibility.is_public?(object)},
likes <- Utils.get_object_likes(object) do
conn
|> put_resp_header("content-type", "application/activity+json")
@ -89,7 +93,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def activity(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :activity, uuid),
%Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:public?, ActivityPub.is_public?(activity)} do
{_, true} <- {:public?, Visibility.is_public?(activity)} do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ObjectView.render("object.json", %{object: activity}))
@ -152,13 +156,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController 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)
Federator.incoming_ap_doc(params)
json(conn, "ok")
end
end
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
Federator.enqueue(:incoming_ap_doc, params)
Federator.incoming_ap_doc(params)
json(conn, "ok")
end

View file

@ -6,40 +6,80 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF
defp delist_message(message) do
defp delist_message(message, threshold) when threshold > 0 do
follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address
follower_collection? = Enum.member?(message["to"] ++ message["cc"], follower_collection)
message =
case get_recipient_count(message) do
{:public, recipients}
when follower_collection? and recipients > threshold ->
message
|> Map.put("to", [follower_collection])
|> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"])
{:public, recipients} when recipients > threshold ->
message
|> Map.put("to", [])
|> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"])
_ ->
message
end
{:ok, message}
end
defp delist_message(message, _threshold), do: {:ok, message}
defp reject_message(message, threshold) when threshold > 0 do
with {_, recipients} <- get_recipient_count(message) do
if recipients > threshold do
{:reject, nil}
else
{:ok, message}
end
end
end
defp reject_message(message, _threshold), do: {:ok, message}
defp get_recipient_count(message) do
recipients = (message["to"] || []) ++ (message["cc"] || [])
follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address
if Enum.member?(recipients, "https://www.w3.org/ns/activitystreams#Public") do
recipients =
recipients
|> List.delete("https://www.w3.org/ns/activitystreams#Public")
|> List.delete(follower_collection)
{:public, length(recipients)}
else
recipients =
recipients
|> List.delete(follower_collection)
{:not_public, length(recipients)}
end
end
@impl true
def filter(%{"type" => "Create"} = message) do
delist_threshold = Pleroma.Config.get([:mrf_hellthread, :delist_threshold])
reject_threshold =
Pleroma.Config.get(
[:mrf_hellthread, :reject_threshold],
Pleroma.Config.get([:mrf_hellthread, :threshold])
)
recipients = (message["to"] || []) ++ (message["cc"] || [])
delist_threshold = Pleroma.Config.get([:mrf_hellthread, :delist_threshold])
cond do
length(recipients) > reject_threshold and reject_threshold > 0 ->
{:reject, nil}
length(recipients) > delist_threshold and delist_threshold > 0 ->
if Enum.member?(message["to"], "https://www.w3.org/ns/activitystreams#Public") or
Enum.member?(message["cc"], "https://www.w3.org/ns/activitystreams#Public") do
{:ok, delist_message(message)}
with {:ok, message} <- reject_message(message, reject_threshold),
{:ok, message} <- delist_message(message, delist_threshold) do
{:ok, message}
else
{:ok, message}
end
true ->
{:ok, message}
_e -> {:reject, nil}
end
end

View file

@ -0,0 +1,81 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF
defp string_matches?(string, pattern) when is_binary(pattern) do
String.contains?(string, pattern)
end
defp string_matches?(string, pattern) do
String.match?(string, pattern)
end
defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = message) do
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
string_matches?(content, pattern) or string_matches?(summary, pattern)
end) do
{:reject, nil}
else
{:ok, message}
end
end
defp check_ftl_removal(
%{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message
) do
if "https://www.w3.org/ns/activitystreams#Public" in to and
Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
string_matches?(content, pattern) or string_matches?(summary, pattern)
end) do
to = List.delete(to, "https://www.w3.org/ns/activitystreams#Public")
cc = ["https://www.w3.org/ns/activitystreams#Public" | message["cc"] || []]
message =
message
|> Map.put("to", to)
|> Map.put("cc", cc)
{:ok, message}
else
{:ok, message}
end
end
defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
{content, summary} =
Enum.reduce(Pleroma.Config.get([:mrf_keyword, :replace]), {content, summary}, fn {pattern,
replacement},
{content_acc,
summary_acc} ->
{String.replace(content_acc, pattern, replacement),
String.replace(summary_acc, pattern, replacement)}
end)
{:ok,
message
|> put_in(["object", "content"], content)
|> put_in(["object", "summary"], summary)}
end
@impl true
def filter(%{"object" => %{"content" => nil}} = message) do
{:ok, message}
end
@impl true
def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do
with {:ok, message} <- check_reject(message),
{:ok, message} <- check_ftl_removal(message),
{:ok, message} <- check_replace(message) do
{:ok, message}
else
_e ->
{:reject, nil}
end
end
@impl true
def filter(message), do: {:ok, message}
end

View file

@ -3,7 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Relay do
alias Pleroma.{User, Object, Activity}
alias Pleroma.User
alias Pleroma.Object
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.ActivityPub
require Logger

View file

@ -6,12 +6,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Object
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
import Ecto.Query
@ -406,7 +407,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
if not User.locked?(followed) do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed.ap_id,
actor: followed,
object: data,
local: true
})
@ -432,7 +433,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed.ap_id,
actor: followed,
object: follow_activity.data["id"],
local: false
}) do
@ -458,7 +459,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
ActivityPub.reject(%{
to: follow_activity.data["to"],
type: "Reject",
actor: followed.ap_id,
actor: followed,
object: follow_activity.data["id"],
local: false
}) do
@ -489,7 +490,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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),
public <- ActivityPub.is_public?(data),
public <- Visibility.is_public?(data),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
{:ok, activity}
else
@ -649,7 +650,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
if object = Object.normalize(id), do: {:ok, object}, else: nil
end
def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do
def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) when is_binary(inReplyTo) do
with false <- String.starts_with?(inReplyTo, "http"),
{:ok, %{data: replied_to_object}} <- get_obj_helper(inReplyTo) do
Map.put(object, "inReplyTo", replied_to_object["external_url"] || inReplyTo)
@ -765,12 +766,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def add_hashtags(object) do
tags =
(object["tag"] || [])
|> Enum.map(fn tag ->
|> Enum.map(fn
# Expand internal representation tags into AS2 tags.
tag when is_binary(tag) ->
%{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
"name" => "##{tag}",
"type" => "Hashtag"
}
# Do not process tags which are already AS2 tag objects.
tag when is_map(tag) ->
tag
end)
object

View file

@ -3,11 +3,19 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.{Repo, Web, Object, Activity, User, Notification}
alias Pleroma.Repo
alias Pleroma.Web
alias Pleroma.Object
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Notification
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Endpoint
alias Ecto.{Changeset, UUID}
alias Ecto.Changeset
alias Ecto.UUID
import Ecto.Query
require Logger
@supported_object_types ["Article", "Note", "Video", "Page"]
@ -156,7 +164,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
_ -> 5
end
Pleroma.Web.Federator.enqueue(:publish, activity, priority)
Pleroma.Web.Federator.publish(activity, priority)
:ok
end
@ -590,4 +598,20 @@ defmodule Pleroma.Web.ActivityPub.Utils do
}
|> Map.merge(additional)
end
#### Flag-related helpers
def make_flag_data(params, additional) do
status_ap_ids = Enum.map(params.statuses || [], & &1.data["id"])
object = [params.account.ap_id] ++ status_ap_ids
%{
"type" => "Flag",
"actor" => params.actor.ap_id,
"content" => params.content,
"object" => object,
"context" => params.context
}
|> Map.merge(additional)
end
end

View file

@ -4,7 +4,8 @@
defmodule Pleroma.Web.ActivityPub.ObjectView do
use Pleroma.Web, :view
alias Pleroma.{Object, Activity}
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Transmogrifier
def render("object.json", %{object: %Object{} = object}) do

View file

@ -4,15 +4,34 @@
defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Salmon
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Endpoint
import Ecto.Query
def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do
%{"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)}
end
def render("endpoints.json", %{user: %User{local: true} = _user}) do
%{
"oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
"oauthRegistrationEndpoint" => Helpers.mastodon_api_url(Endpoint, :create_app),
"oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)
}
end
def render("endpoints.json", _), do: %{}
# the instance itself is not a Person, but instead an Application
def render("user.json", %{user: %{nickname: nil} = user}) do
{:ok, user} = WebFinger.ensure_keys_present(user)
@ -20,6 +39,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
endpoints = render("endpoints.json", %{user: user})
%{
"id" => user.ap_id,
"type" => "Application",
@ -35,9 +56,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => %{
"sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
}
"endpoints" => endpoints
}
|> Map.merge(Utils.make_json_ld_header())
end
@ -48,6 +67,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
endpoints = render("endpoints.json", %{user: user})
%{
"id" => user.ap_id,
"type" => "Person",
@ -65,9 +86,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => %{
"sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
},
"endpoints" => endpoints,
"icon" => %{
"type" => "Image",
"url" => User.avatar_url(user)
@ -86,7 +105,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
collection(following, "#{user.ap_id}/following", page, !user.info.hide_follows)
total =
if !user.info.hide_follows do
length(following)
else
0
end
collection(following, "#{user.ap_id}/following", page, !user.info.hide_follows, total)
|> Map.merge(Utils.make_json_ld_header())
end
@ -95,10 +121,17 @@ defmodule Pleroma.Web.ActivityPub.UserView do
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
total =
if !user.info.hide_follows do
length(following)
else
0
end
%{
"id" => "#{user.ap_id}/following",
"type" => "OrderedCollection",
"totalItems" => length(following),
"totalItems" => total,
"first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows)
}
|> Map.merge(Utils.make_json_ld_header())
@ -109,7 +142,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_followers)
total =
if !user.info.hide_followers do
length(followers)
else
0
end
collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_followers, total)
|> Map.merge(Utils.make_json_ld_header())
end
@ -118,19 +158,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
total =
if !user.info.hide_followers do
length(followers)
else
0
end
%{
"id" => "#{user.ap_id}/followers",
"type" => "OrderedCollection",
"totalItems" => length(followers),
"first" => collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_followers)
"totalItems" => total,
"first" =>
collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_followers, total)
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("outbox.json", %{user: user, max_id: max_qid}) do
# XXX: technically note_count is wrong for this, but it's better than nothing
info = User.user_info(user)
params = %{
"limit" => "10"
}
@ -143,14 +188,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do
end
activities = ActivityPub.fetch_user_activities(user, nil, params)
min_id = Enum.at(Enum.reverse(activities), 0).id
max_id = Enum.at(activities, 0).id
collection =
{max_id, min_id, collection} =
if length(activities) > 0 do
{
Enum.at(Enum.reverse(activities), 0).id,
Enum.at(activities, 0).id,
Enum.map(activities, fn act ->
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
data
end)
}
else
{
0,
0,
[]
}
end
iri = "#{user.ap_id}/outbox"
@ -158,7 +213,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"id" => "#{iri}?max_id=#{max_id}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"totalItems" => info.note_count,
"orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id}"
}
@ -167,7 +221,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
%{
"id" => iri,
"type" => "OrderedCollection",
"totalItems" => info.note_count,
"first" => page
}
|> Map.merge(Utils.make_json_ld_header())
@ -205,7 +258,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"id" => "#{iri}?max_id=#{max_id}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"totalItems" => -1,
"orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id}"
}
@ -214,7 +266,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
%{
"id" => iri,
"type" => "OrderedCollection",
"totalItems" => -1,
"first" => page
}
|> Map.merge(Utils.make_json_ld_header())

View file

@ -0,0 +1,56 @@
defmodule Pleroma.Web.ActivityPub.Visibility do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
def is_public?(%Object{data: data}), do: is_public?(data)
def is_public?(%Activity{data: data}), do: is_public?(data)
def is_public?(%{"directMessage" => true}), do: false
def is_public?(data) do
"https://www.w3.org/ns/activitystreams#Public" in (data["to"] ++ (data["cc"] || []))
end
def is_private?(activity) do
unless is_public?(activity) do
follower_address = User.get_cached_by_ap_id(activity.data["actor"]).follower_address
Enum.any?(activity.data["to"], &(&1 == follower_address))
else
false
end
end
def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
def is_direct?(%Object{data: %{"directMessage" => true}}), do: true
def is_direct?(activity) do
!is_public?(activity) && !is_private?(activity)
end
def visible_for_user?(activity, nil) do
is_public?(activity)
end
def visible_for_user?(activity, user) do
x = [user.ap_id | user.following]
y = [activity.actor] ++ 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)
end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.TwitterAPI.UserView
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@ -41,6 +42,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> json(user.nickname)
end
def user_toggle_activation(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname)
{:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
conn
|> json(UserView.render("show_for_admin.json", %{user: updated_user}))
end
def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags),
do: json_response(conn, :no_content, "")
@ -51,6 +61,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
do: json_response(conn, :no_content, "")
end
def list_users(%{assigns: %{user: admin}} = conn, _data) do
users = User.all_except_one(admin)
conn
|> json(UserView.render("index_for_admin.json", %{users: users}))
end
def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
when permission_group in ["moderator", "admin"] do
user = User.get_by_nickname(nickname)
@ -124,6 +141,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> json(%{error: "No such permission_group"})
end
def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do
with {:ok, status} <- Ecto.Type.cast(:boolean, status),
%User{} = user <- User.get_by_nickname(nickname),
{:ok, _} <- User.deactivate(user, !status),
do: json_response(conn, :no_content, "")
end
def relay_follow(conn, %{"relay_url" => target}) do
with {:ok, _message} <- Relay.follow(target) do
json(conn, target)

View file

@ -0,0 +1,25 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.Authenticator do
alias Pleroma.User
def implementation do
Pleroma.Config.get(
Pleroma.Web.Auth.Authenticator,
Pleroma.Web.Auth.PleromaAuthenticator
)
end
@callback get_user(Plug.Conn.t()) :: {:ok, User.t()} | {:error, any()}
def get_user(plug), do: implementation().get_user(plug)
@callback handle_error(Plug.Conn.t(), any()) :: any()
def handle_error(plug, error), do: implementation().handle_error(plug, error)
@callback auth_template() :: String.t() | nil
def auth_template do
implementation().auth_template() || Pleroma.Config.get(:auth_template, "show.html")
end
end

View file

@ -0,0 +1,28 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.PleromaAuthenticator do
alias Pleroma.User
alias Comeonin.Pbkdf2
@behaviour Pleroma.Web.Auth.Authenticator
def get_user(%Plug.Conn{} = conn) do
%{"authorization" => %{"name" => name, "password" => password}} = conn.params
with {_, %User{} = user} <- {:user, User.get_by_nickname_or_email(name)},
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do
{:ok, user}
else
error ->
{:error, error}
end
end
def handle_error(%Plug.Conn{} = _conn, error) do
error
end
def auth_template, do: nil
end

View file

@ -3,7 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI do
alias Pleroma.{User, Repo, Activity, Object}
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Formatter
@ -78,40 +82,20 @@ defmodule Pleroma.Web.CommonAPI do
def get_visibility(_), do: "public"
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),
attachments <- attachments_from_ids(data["media_ids"]),
mentions <- Formatter.parse_mentions(status),
attachments <- attachments_from_ids(data),
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 <-
{content_html, mentions, tags} <-
make_content_html(
status,
mentions,
attachments,
tags,
get_content_type(data["content_type"]),
Enum.member?(
[true, "true"],
Map.get(
data,
"no_attachment_links",
Pleroma.Config.get([:instance, :no_attachment_links], false)
)
)
data
),
{to, cc} <- to_for_user_and_mentions(user, mentions, inReplyTo, visibility),
context <- make_context(inReplyTo),
cw <- data["spoiler_text"],
full_payload <- String.trim(status <> (data["spoiler_text"] || "")),
@ -216,4 +200,54 @@ defmodule Pleroma.Web.CommonAPI do
{:error, "Could not unpin"}
end
end
def add_mute(user, activity) do
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
{:ok, activity}
else
{:error, _} -> {:error, "conversation is already muted"}
end
end
def remove_mute(user, activity) do
ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity}
end
def thread_muted?(%{id: nil} = _user, _activity), do: false
def thread_muted?(user, activity) do
with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
false
else
_ -> true
end
end
def report(user, data) do
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
{:account, %User{} = account} <- {:account, User.get_by_id(account_id)},
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
{:ok, statuses} <- get_report_statuses(account, data),
{:ok, activity} <-
ActivityPub.flag(%{
context: Utils.generate_context_id(),
actor: user,
account: account,
statuses: statuses,
content: content_html
}) do
Enum.each(User.all_superusers(), fn superuser ->
superuser
|> Pleroma.AdminEmail.report(user, account, statuses, content_html)
|> Pleroma.Mailer.deliver_async()
end)
{:ok, activity}
else
{:error, err} -> {:error, err}
{:account_id, %{}} -> {:error, "Valid `account_id` required"}
{:account, nil} -> {:error, "Account not found"}
end
end
end

View file

@ -5,12 +5,15 @@
defmodule Pleroma.Web.CommonAPI.Utils do
alias Calendar.Strftime
alias Comeonin.Pbkdf2
alias Pleroma.{Activity, Formatter, Object, Repo}
alias Pleroma.Activity
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Config
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.ActivityPub.Utils
# This is a hack for twidere.
def get_by_id_or_ap_id(id) do
@ -32,12 +35,28 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def get_replied_to_activity(_), do: nil
def attachments_from_ids(ids) do
def attachments_from_ids(data) do
if Map.has_key?(data, "descriptions") do
attachments_from_ids_descs(data["media_ids"], data["descriptions"])
else
attachments_from_ids_no_descs(data["media_ids"])
end
end
def attachments_from_ids_no_descs(ids) do
Enum.map(ids || [], fn media_id ->
Repo.get(Object, media_id).data
end)
end
def attachments_from_ids_descs(ids, descs_str) do
{_, descs} = Jason.decode(descs_str)
Enum.map(ids || [], fn media_id ->
Map.put(Repo.get(Object, media_id).data, "name", descs[media_id])
end)
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
@ -81,24 +100,45 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def make_content_html(
status,
mentions,
attachments,
tags,
content_type,
no_attachment_links \\ false
data
) do
no_attachment_links =
data
|> Map.get("no_attachment_links", Config.get([:instance, :no_attachment_links]))
|> Kernel.in([true, "true"])
content_type = get_content_type(data["content_type"])
status
|> format_input(mentions, tags, content_type)
|> format_input(content_type)
|> maybe_add_attachments(attachments, no_attachment_links)
|> maybe_add_nsfw_tag(data)
end
defp get_content_type(content_type) do
if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do
content_type
else
"text/plain"
end
end
defp maybe_add_nsfw_tag({text, mentions, tags}, %{"sensitive" => sensitive})
when sensitive in [true, "True", "true", "1"] do
{text, mentions, [{"#nsfw", "nsfw"} | tags]}
end
defp maybe_add_nsfw_tag(data, _), do: data
def make_context(%Activity{data: %{"context" => context}}), do: context
def make_context(_), do: Utils.generate_context_id()
def maybe_add_attachments(text, _attachments, _no_links = true), do: text
def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed
def maybe_add_attachments(text, attachments, _no_links) do
add_attachments(text, attachments)
def maybe_add_attachments({text, mentions, tags}, attachments, _no_links) do
text = add_attachments(text, attachments)
{text, mentions, tags}
end
def add_attachments(text, attachments) do
@ -116,56 +156,39 @@ defmodule Pleroma.Web.CommonAPI.Utils do
Enum.join([text | attachment_text], "<br>")
end
def format_input(text, mentions, tags, format, options \\ [])
def format_input(text, format, options \\ [])
@doc """
Formatting text to plain text.
"""
def format_input(text, mentions, tags, "text/plain", options) do
def format_input(text, "text/plain", options) do
text
|> Formatter.html_escape("text/plain")
|> String.replace(~r/\r?\n/, "<br>")
|> (&{[], &1}).()
|> Formatter.add_links()
|> Formatter.add_user_links(mentions, options[:user_links] || [])
|> Formatter.add_hashtag_links(tags)
|> Formatter.finalize()
|> Formatter.linkify(options)
|> (fn {text, mentions, tags} ->
{String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
end).()
end
@doc """
Formatting text to html.
"""
def format_input(text, mentions, _tags, "text/html", options) do
def format_input(text, "text/html", options) do
text
|> Formatter.html_escape("text/html")
|> (&{[], &1}).()
|> Formatter.add_user_links(mentions, options[:user_links] || [])
|> Formatter.finalize()
|> Formatter.linkify(options)
end
@doc """
Formatting text to markdown.
"""
def format_input(text, mentions, tags, "text/markdown", options) do
def format_input(text, "text/markdown", options) do
options = Keyword.put(options, :mentions_escape, true)
text
|> Formatter.mentions_escape(mentions)
|> Earmark.as_html!()
|> Formatter.linkify(options)
|> (fn {text, mentions, tags} -> {Earmark.as_html!(text), mentions, tags} end).()
|> Formatter.html_escape("text/html")
|> (&{[], &1}).()
|> Formatter.add_user_links(mentions, options[:user_links] || [])
|> Formatter.add_hashtag_links(tags)
|> Formatter.finalize()
end
def add_tag_links(text, tags) do
tags =
tags
|> Enum.sort_by(fn {tag, _} -> -String.length(tag) end)
Enum.reduce(tags, text, fn {full, tag}, text ->
url = "<a href='#{Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
String.replace(text, full, url)
end)
end
def make_note_data(
@ -303,4 +326,22 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
def maybe_extract_mentions(_), do: []
def make_report_content_html(nil), do: {:ok, {nil, [], []}}
def make_report_content_html(comment) do
max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
if String.length(comment) <= max_size do
{:ok, format_input(comment, "text/plain")}
else
{:error, "Comment must be up to #{max_size} characters"}
end
end
def get_report_statuses(%User{ap_id: actor}, %{"status_ids" => status_ids}) do
{:ok, Activity.all_by_actor_and_id(actor, status_ids)}
end
def get_report_statuses(_, _), do: {:ok, nil}
end

View file

@ -5,6 +5,11 @@
defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller
def oauth_scopes(params, default) do
# Note: `scopes` is used by Mastodon — supporting it but sticking to OAuth's standard `scope` wherever we control it
Pleroma.Web.OAuth.parse_scopes(params["scope"] || params["scopes"], default)
end
def json_response(conn, status, json) do
conn
|> put_status(status)

View file

@ -3,54 +3,83 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator do
use GenServer
alias Pleroma.User
alias Pleroma.Activity
alias Pleroma.Web.{WebFinger, Websub, Salmon}
alias Pleroma.Web.Federator.RetryQueue
alias Pleroma.User
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub
alias Pleroma.Web.Salmon
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator.RetryQueue
alias Pleroma.Web.OStatus
alias Pleroma.Jobs
require Logger
@websub Application.get_env(:pleroma, :websub)
@ostatus Application.get_env(:pleroma, :ostatus)
def init(args) do
{:ok, args}
end
def start_link do
spawn(fn ->
def init() do
# 1 minute
Process.sleep(1000 * 60 * 1)
enqueue(:refresh_subscriptions, nil)
end)
GenServer.start_link(
__MODULE__,
%{
in: {:sets.new(), []},
out: {:sets.new(), []}
},
name: __MODULE__
)
refresh_subscriptions()
end
def handle(:refresh_subscriptions, _) do
# Client API
def incoming_doc(doc) do
Jobs.enqueue(:federator_incoming, __MODULE__, [:incoming_doc, doc])
end
def incoming_ap_doc(params) do
Jobs.enqueue(:federator_incoming, __MODULE__, [:incoming_ap_doc, params])
end
def publish(activity, priority \\ 1) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)
end
def publish_single_ap(params) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params])
end
def publish_single_websub(websub) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub])
end
def verify_websub(websub) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])
end
def request_subscription(sub) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub])
end
def refresh_subscriptions() do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
end
def publish_single_salmon(params) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])
end
# Job Worker Callbacks
def perform(:refresh_subscriptions) do
Logger.debug("Federator running refresh subscriptions")
Websub.refresh_subscriptions()
spawn(fn ->
# 6 hours
Process.sleep(1000 * 60 * 60 * 6)
enqueue(:refresh_subscriptions, nil)
refresh_subscriptions()
end)
end
def handle(:request_subscription, websub) do
def perform(:request_subscription, websub) do
Logger.debug("Refreshing #{websub.topic}")
with {:ok, websub} <- Websub.request_subscription(websub) do
@ -60,13 +89,13 @@ defmodule Pleroma.Web.Federator do
end
end
def handle(:publish, activity) do
def perform(:publish, activity) do
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
{:ok, actor} = WebFinger.ensure_keys_present(actor)
if ActivityPub.is_public?(activity) do
if Visibility.is_public?(activity) do
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)
@ -86,7 +115,7 @@ defmodule Pleroma.Web.Federator do
end
end
def handle(:verify_websub, websub) do
def perform(:verify_websub, websub) do
Logger.debug(fn ->
"Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})"
end)
@ -94,12 +123,12 @@ defmodule Pleroma.Web.Federator do
@websub.verify(websub)
end
def handle(:incoming_doc, doc) do
def perform(:incoming_doc, doc) do
Logger.info("Got document, trying to parse")
@ostatus.handle_incoming(doc)
end
def handle(:incoming_ap_doc, params) do
def perform(:incoming_ap_doc, params) do
Logger.info("Handling incoming AP activity")
params = Utils.normalize_params(params)
@ -124,11 +153,11 @@ defmodule Pleroma.Web.Federator do
end
end
def handle(:publish_single_salmon, params) do
def perform(:publish_single_salmon, params) do
Salmon.send_to_user(params)
end
def handle(:publish_single_ap, params) do
def perform(:publish_single_ap, params) do
case ActivityPub.publish_one(params) do
{:ok, _} ->
:ok
@ -138,7 +167,7 @@ defmodule Pleroma.Web.Federator do
end
end
def handle(
def perform(
:publish_single_websub,
%{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params
) do
@ -151,75 +180,11 @@ defmodule Pleroma.Web.Federator do
end
end
def handle(type, _) do
def perform(type, _) do
Logger.debug(fn -> "Unknown task: #{type}" end)
{:error, "Don't know what to do with this"}
end
if Mix.env() == :test do
def enqueue(type, payload, _priority \\ 1) do
if Pleroma.Config.get([:instance, :federating]) do
handle(type, payload)
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
end
def maybe_start_job(running_jobs, queue) do
if :sets.size(running_jobs) < Pleroma.Config.get([__MODULE__, :max_jobs]) && queue != [] do
{{type, payload}, queue} = queue_pop(queue)
{:ok, pid} = Task.start(fn -> handle(type, payload) end)
mref = Process.monitor(pid)
{:sets.add_element(mref, running_jobs), queue}
else
{running_jobs, queue}
end
end
def handle_cast({:enqueue, type, payload, _priority}, state)
when type in [:incoming_doc, :incoming_ap_doc] do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
i_queue = enqueue_sorted(i_queue, {type, payload}, 1)
{i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end
def handle_cast({:enqueue, type, payload, _priority}, state) do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
o_queue = enqueue_sorted(o_queue, {type, payload}, 1)
{o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue)
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end
def handle_cast(m, state) do
IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}")
{:noreply, state}
end
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
i_running_jobs = :sets.del_element(ref, i_running_jobs)
o_running_jobs = :sets.del_element(ref, o_running_jobs)
{i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
{o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue)
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end
def enqueue_sorted(queue, element, priority) do
[%{item: element, priority: priority} | queue]
|> Enum.sort_by(fn %{priority: priority} -> priority end)
end
def queue_pop([%{item: element} | queue]) do
{element, queue}
end
def ap_enabled_actor(id) do
user = User.get_by_ap_id(id)

View file

@ -5,8 +5,9 @@
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
defmodule Pleroma.Web.HTTPSignatures do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
require Logger
def split_signature(sig) do

View file

@ -4,34 +4,55 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Filter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.Push
alias Push.Subscription
alias Pleroma.Web.MastodonAPI.{
StatusView,
AccountView,
MastodonView,
ListView,
FilterView,
PushSubscriptionView
}
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.PushSubscriptionView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MastodonAPI.ReportView
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth.{Authorization, Token, App}
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
import Ecto.Query
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
@local_mastodon_name "Mastodon-Local"
action_fallback(:errors)
def create_app(conn, params) do
with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
{:ok, app} <- Repo.insert(cs) |> IO.inspect() do
scopes = oauth_scopes(params, ["read"])
app_attrs =
params
|> Map.drop(["scope", "scopes"])
|> Map.put("scopes", scopes)
with cs <- App.register_changeset(%App{}, app_attrs),
false <- cs.changes[:client_name] == @local_mastodon_name,
{:ok, app} <- Repo.insert(cs) do
res = %{
id: app.id |> to_string,
name: app.client_name,
@ -129,7 +150,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
@mastodon_api_level "2.5.0"
def masto_instance(conn, _params) do
instance = Pleroma.Config.get(:instance)
instance = Config.get(:instance)
response = %{
uri: Web.base_url(),
@ -222,10 +243,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
activities =
ActivityPub.fetch_activities([user.ap_id | user.following], params)
[user.ap_id | user.following]
|> ActivityPub.fetch_activities(params)
|> ActivityPub.contain_timeline(user)
|> Enum.reverse()
@ -238,14 +261,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def public_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"]
params =
activities =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_public_activities(params)
|> Map.put("muting_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn
@ -286,7 +308,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
true <- Visibility.visible_for_user?(activity, user) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user})
@ -314,6 +336,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
as: :activity
)
|> Enum.reverse(),
# credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
descendants:
StatusView.render(
"index.json",
@ -322,6 +345,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
as: :activity
)
|> Enum.reverse()
# credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
}
json(conn, result)
@ -426,7 +450,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
%User{} = user <- User.get_by_nickname(user.nickname),
true <- ActivityPub.visible_for_user?(activity, user),
true <- Visibility.visible_for_user?(activity, user),
{:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
conn
|> put_view(StatusView)
@ -437,7 +461,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
%User{} = user <- User.get_by_nickname(user.nickname),
true <- ActivityPub.visible_for_user?(activity, user),
true <- Visibility.visible_for_user?(activity, user),
{:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
conn
|> put_view(StatusView)
@ -445,13 +469,37 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
activity = Activity.get_by_id(id)
with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
else
{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
end
end
def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
activity = Activity.get_by_id(id)
with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params)
result =
Enum.map(notifications, fn x ->
render_notification(user, x)
end)
notifications
|> Enum.map(fn x -> render_notification(user, x) end)
|> Enum.filter(& &1)
conn
@ -580,17 +628,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
[]
|> Enum.map(&String.downcase(&1))
query_params =
activities =
params
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("tag", tags)
|> Map.put("tag_all", tag_all)
|> Map.put("tag_reject", tag_reject)
activities =
ActivityPub.fetch_public_activities(query_params)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn
@ -647,7 +694,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed.ap_id,
actor: followed,
object: follow_activity.data["id"],
type: "Accept"
}) do
@ -669,7 +716,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed.ap_id,
actor: followed,
object: follow_activity.data["id"],
type: "Reject"
}) do
@ -690,7 +737,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
{:ok, _activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
@ -730,6 +777,41 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
with %User{} = muted <- Repo.get(User, id),
{:ok, muter} <- User.mute(muter, muted) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: muter, target: muted})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
with %User{} = muted <- Repo.get(User, id),
{:ok, muter} <- User.unmute(muter, muted) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: muter, target: muted})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def mutes(%{assigns: %{user: user}} = conn, _) do
with muted_accounts <- User.muted_users(user) do
res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
json(conn, res)
end
end
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.block(blocker, blocked),
@ -786,7 +868,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
if Regex.match?(~r/https?:/, query) do
with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- ActivityPub.visible_for_user?(activity, user) do
true <- Visibility.visible_for_user?(activity, user) do
[activity]
else
_e -> []
@ -819,7 +901,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
tags_path = Web.base_url() <> "/tag/"
tags =
String.split(query)
query
|> String.split()
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
@ -841,7 +924,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
statuses = status_search(user, query)
tags =
String.split(query)
query
|> String.split()
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
@ -865,14 +949,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def favourites(%{assigns: %{user: user}} = conn, params) do
params =
activities =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_public_activities(params)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
conn
@ -985,15 +1067,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
# we must filter the following list for the user to avoid leaking statuses the user
# does not actually have permission to see (for more info, peruse security issue #270).
following_to =
activities =
following
|> Enum.filter(fn x -> x in user.following end)
activities =
ActivityPub.fetch_activities_bounded(following_to, following, params)
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()
conn
@ -1015,11 +1096,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
if user && token do
mastodon_emoji = mastodonized_emoji()
limit = Pleroma.Config.get([:instance, :limit])
limit = Config.get([:instance, :limit])
accounts =
Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
flavour = get_user_flavour(user)
initial_state =
%{
meta: %{
@ -1039,8 +1122,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
max_toot_chars: limit
},
rights: %{
delete_others_notice: !!user.info.is_moderator,
admin: !!user.info.is_admin
delete_others_notice: present?(user.info.is_moderator),
admin: present?(user.info.is_admin)
},
compose: %{
me: "#{user.id}",
@ -1104,7 +1187,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
conn
|> put_layout(false)
|> put_view(MastodonView)
|> render("index.html", %{initial_state: initial_state})
|> render("index.html", %{initial_state: initial_state, flavour: flavour})
else
conn
|> redirect(to: "/web/login")
@ -1126,6 +1209,43 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
@supported_flavours ["glitch", "vanilla"]
def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
when flavour in @supported_flavours do
flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
with changeset <- Ecto.Changeset.change(user),
changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
{:ok, user} <- User.update_and_set_cache(changeset),
flavour <- user.info.flavour do
json(conn, flavour)
else
e ->
conn
|> put_resp_content_type("application/json")
|> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
end
end
def set_flavour(conn, _params) do
conn
|> put_status(400)
|> json(%{error: "Unsupported flavour"})
end
def get_flavour(%{assigns: %{user: user}} = conn, _params) do
json(conn, get_user_flavour(user))
end
defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
flavour
end
defp get_user_flavour(_) do
"glitch"
end
def login(conn, %{"code" => code}) do
with {:ok, app} <- get_or_make_app(),
%Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
@ -1145,7 +1265,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
response_type: "code",
client_id: app.client_id,
redirect_uri: ".",
scope: app.scopes
scope: Enum.join(app.scopes, " ")
)
conn
@ -1154,16 +1274,27 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
defp get_or_make_app() do
with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do
find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
scopes = ["read", "write", "follow", "push"]
with %App{} = app <- Repo.get_by(App, find_attrs) do
{:ok, app} =
if app.scopes == scopes do
{:ok, app}
else
app
|> Ecto.Changeset.change(%{scopes: scopes})
|> Repo.update()
end
{:ok, app}
else
_e ->
cs =
App.register_changeset(%App{}, %{
client_name: "Mastodon-Local",
redirect_uris: ".",
scopes: "read,write,follow"
})
App.register_changeset(
%App{},
Map.put(find_attrs, :scopes, scopes)
)
Repo.insert(cs)
end
@ -1235,7 +1366,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def get_filters(%{assigns: %{user: user}} = conn, _) do
filters = Pleroma.Filter.get_filters(user)
filters = Filter.get_filters(user)
res = FilterView.render("filters.json", filters: filters)
json(conn, res)
end
@ -1244,7 +1375,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context} = params
) do
query = %Pleroma.Filter{
query = %Filter{
user_id: user.id,
phrase: phrase,
context: context,
@ -1253,13 +1384,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
# expires_at
}
{:ok, response} = Pleroma.Filter.create(query)
{:ok, response} = Filter.create(query)
res = FilterView.render("filter.json", filter: response)
json(conn, res)
end
def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
filter = Pleroma.Filter.get(filter_id, user)
filter = Filter.get(filter_id, user)
res = FilterView.render("filter.json", filter: filter)
json(conn, res)
end
@ -1268,7 +1399,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context, "id" => filter_id} = params
) do
query = %Pleroma.Filter{
query = %Filter{
user_id: user.id,
filter_id: filter_id,
phrase: phrase,
@ -1278,32 +1409,32 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
# expires_at
}
{:ok, response} = Pleroma.Filter.update(query)
{:ok, response} = Filter.update(query)
res = FilterView.render("filter.json", filter: response)
json(conn, res)
end
def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
query = %Pleroma.Filter{
query = %Filter{
user_id: user.id,
filter_id: filter_id
}
{:ok, _} = Pleroma.Filter.delete(query)
{:ok, _} = Filter.delete(query)
json(conn, %{})
end
def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
true = Pleroma.Web.Push.enabled()
Pleroma.Web.Push.Subscription.delete_if_exists(user, token)
{:ok, subscription} = Pleroma.Web.Push.Subscription.create(user, token, params)
true = Push.enabled()
Subscription.delete_if_exists(user, token)
{:ok, subscription} = Subscription.create(user, token, params)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
true = Pleroma.Web.Push.enabled()
subscription = Pleroma.Web.Push.Subscription.get(user, token)
true = Push.enabled()
subscription = Subscription.get(user, token)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
@ -1312,15 +1443,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
%{assigns: %{user: user, token: token}} = conn,
params
) do
true = Pleroma.Web.Push.enabled()
{:ok, subscription} = Pleroma.Web.Push.Subscription.update(user, token, params)
true = Push.enabled()
{:ok, subscription} = Subscription.update(user, token, params)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
true = Pleroma.Web.Push.enabled()
{:ok, _response} = Pleroma.Web.Push.Subscription.delete(user, token)
true = Push.enabled()
{:ok, _response} = Subscription.delete(user, token)
json(conn, %{})
end
@ -1331,17 +1462,21 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def suggestions(%{assigns: %{user: user}} = conn, _) do
suggestions = Pleroma.Config.get(:suggestions)
suggestions = 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 = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
host = Config.get([Pleroma.Web.Endpoint, :url, :host])
user = user.nickname
url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
url =
api
|> String.replace("{{host}}", host)
|> String.replace("{{user}}", user)
with {:ok, %{status: 200, body: body}} <-
@httpoison.get(
@ -1354,8 +1489,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
]
),
{:ok, data} <- Jason.decode(body) do
data2 =
Enum.slice(data, 0, limit)
data =
data
|> Enum.slice(0, limit)
|> Enum.map(fn x ->
Map.put(
x,
@ -1374,7 +1510,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end)
conn
|> json(data2)
|> json(data)
else
e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
end
@ -1383,9 +1519,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def status_card(conn, %{"id" => status_id}) do
def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
with %Activity{} = activity <- Repo.get(Activity, status_id),
true <- ActivityPub.is_public?(activity) do
true <- Visibility.visible_for_user?(activity, user) do
data =
StatusView.render(
"card.json",
@ -1399,6 +1535,20 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def reports(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.report(user, params) do
{:ok, activity} ->
conn
|> put_view(ReportView)
|> try_render("report.json", %{activity: activity})
{:error, err} ->
conn
|> put_status(:bad_request)
|> json(%{error: err})
end
end
def try_render(conn, target, params)
when is_binary(target) do
res = render(conn, target, params)
@ -1417,4 +1567,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> put_status(501)
|> json(%{error: "Can't display this activity"})
end
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
end

View file

@ -4,11 +4,12 @@
defmodule Pleroma.Web.MastodonAPI.AccountView do
use Pleroma.Web, :view
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MediaProxy
def render("accounts.json", %{users: users} = opts) do
users
@ -31,7 +32,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
}
end
def render("relationship.json", %{user: user, target: target}) do
def render("relationship.json", %{user: nil, target: _target}) do
%{}
end
def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do
follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)
requested =
@ -46,7 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
following: User.following?(user, target),
followed_by: User.following?(target, user),
blocking: User.blocks?(user, target),
muting: false,
muting: User.mutes?(user, target),
muting_notifications: false,
requested: requested,
domain_blocking: false,
@ -84,6 +89,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
relationship = render("relationship.json", %{user: opts[:for], target: user})
%{
id: to_string(user.id),
username: username_from_nickname(user.nickname),
@ -114,7 +121,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
confirmation_pending: user_info.confirmation_pending,
tags: user.tags,
is_moderator: user.info.is_moderator,
is_admin: user.info.is_admin
is_admin: user.info.is_admin,
relationship: relationship
}
}
end

View file

@ -4,8 +4,8 @@
defmodule Pleroma.Web.MastodonAPI.FilterView do
use Pleroma.Web, :view
alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.FilterView
def render("filters.json", %{filters: filters} = opts) do
render_many(filters, FilterView, "filter.json", opts)

View file

@ -0,0 +1,14 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ReportView do
use Pleroma.Web, :view
def render("report.json", %{activity: activity}) do
%{
id: to_string(activity.id),
action_taken: false
}
end
end

View file

@ -9,10 +9,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.HTML
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
# TODO: Add cached version.
defp get_replied_to_activities(activities) do
@ -143,10 +144,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
url =
if user.local do
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
else
object["external_url"] || object["id"]
end
%{
id: to_string(activity.id),
uri: object["id"],
url: object["external_url"] || object["id"],
url: url,
account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
@ -160,12 +168,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
reblogged: present?(repeated),
favourited: present?(favorited),
bookmarked: present?(bookmarked),
muted: false,
muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user),
pinned: pinned?(activity, user),
sensitive: sensitive,
spoiler_text: object["summary"] || "",
visibility: get_visibility(object),
media_attachments: attachments |> Enum.take(4),
media_attachments: attachments,
mentions: mentions,
tags: build_tags(tags),
application: %{

View file

@ -6,9 +6,10 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
require Logger
alias Pleroma.Web.OAuth.Token
alias Pleroma.{User, Repo}
alias Pleroma.Repo
alias Pleroma.User
@behaviour :cowboy_websocket_handler
@behaviour :cowboy_websocket
@streams [
"public",
@ -25,37 +26,37 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
# Handled by periodic keepalive in Pleroma.Web.Streamer.
@timeout :infinity
def init(_type, _req, _opts) do
{:upgrade, :protocol, :cowboy_websocket}
end
def websocket_init(_type, req, _opts) do
with {qs, req} <- :cowboy_req.qs(req),
params <- :cow_qs.parse_qs(qs),
def init(%{qs: qs} = req, state) do
with params <- :cow_qs.parse_qs(qs),
access_token <- List.keyfind(params, "access_token", 0),
{_, stream} <- List.keyfind(params, "stream", 0),
{:ok, user} <- allow_request(stream, access_token),
topic when is_binary(topic) <- expand_topic(stream, params) do
send(self(), :subscribe)
{:ok, req, %{user: user, topic: topic}, @timeout}
{:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
else
{:error, code} ->
Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
{:ok, req} = :cowboy_req.reply(code, req)
{:shutdown, req}
{:ok, req, state}
error ->
Logger.debug("#{__MODULE__} denied connection: #{inspect(error)} - #{inspect(req)}")
{:shutdown, req}
{:ok, req} = :cowboy_req.reply(400, req)
{:ok, req, state}
end
end
def websocket_init(state) do
send(self(), :subscribe)
{:ok, state}
end
# We never receive messages.
def websocket_handle(_frame, req, state) do
{:ok, req, state}
def websocket_handle(_frame, state) do
{:ok, state}
end
def websocket_info(:subscribe, req, state) do
def websocket_info(:subscribe, state) do
Logger.debug(
"#{__MODULE__} accepted websocket connection for user #{
(state.user || %{id: "anonymous"}).id
@ -63,14 +64,14 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
)
Pleroma.Web.Streamer.add_socket(state.topic, streamer_socket(state))
{:ok, req, state}
{:ok, state}
end
def websocket_info({:text, message}, req, state) do
{:reply, {:text, message}, req, state}
def websocket_info({:text, message}, state) do
{:reply, {:text, message}, state}
end
def websocket_terminate(reason, _req, state) do
def terminate(reason, _req, state) do
Logger.debug(
"#{__MODULE__} terminating websocket connection for user #{
(state.user || %{id: "anonymous"}).id

View file

@ -4,11 +4,12 @@
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
use Pleroma.Web, :controller
alias Pleroma.{Web.MediaProxy, ReverseProxy}
alias Pleroma.ReverseProxy
alias Pleroma.Web.MediaProxy
@default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
def remote(conn, params = %{"sig" => sig64, "url" => url64}) do
def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
with config <- Pleroma.Config.get([:media_proxy], []),
true <- Keyword.get(config, :enabled, false),
{:ok, url} <- MediaProxy.decode_url(sig64, url64),

View file

@ -9,7 +9,7 @@ defmodule Pleroma.Web.MediaProxy do
def url(""), do: nil
def url(url = "/" <> _), do: url
def url("/" <> _ = url), do: url
def url(url) do
config = Application.get_env(:pleroma, :media_proxy, [])
@ -19,11 +19,16 @@ defmodule Pleroma.Web.MediaProxy do
else
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
# Must preserve `%2F` for compatibility with S3 (https://git.pleroma.social/pleroma/pleroma/issues/580)
replacement = get_replacement(url, ":2F:")
# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
base64 =
url
|> String.replace("%2F", replacement)
|> URI.decode()
|> URI.encode()
|> String.replace(replacement, "%2F")
|> Base.url_encode64(@base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
@ -60,4 +65,12 @@ defmodule Pleroma.Web.MediaProxy do
|> Enum.filter(fn value -> value end)
|> Path.join()
end
defp get_replacement(url, replacement) do
if String.contains?(url, replacement) do
get_replacement(url, replacement <> replacement)
else
replacement
end
end
end

View file

@ -3,10 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
alias Pleroma.Web.Metadata.Providers.Provider
alias Pleroma.User
alias Pleroma.Web.Metadata
alias Pleroma.{HTML, Formatter, User}
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.Metadata.Providers.Provider
alias Pleroma.Web.Metadata.Utils
@behaviour Provider
@ -17,7 +17,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
user: user
}) do
attachments = build_attachments(object)
scrubbed_content = scrub_html_and_truncate(object)
scrubbed_content = Utils.scrub_html_and_truncate(object)
# Zero width space
content =
if scrubbed_content != "" and scrubbed_content != "\u200B" do
@ -42,13 +42,14 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
{:meta,
[
property: "og:description",
content: "#{user_name_string(user)}" <> content
content: "#{Utils.user_name_string(user)}" <> content
], []},
{:meta, [property: "og:type", content: "website"], []}
] ++
if attachments == [] or Metadata.activity_nsfw?(object) do
[
{:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []},
{:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))],
[]},
{:meta, [property: "og:image:width", content: 150], []},
{:meta, [property: "og:image:height", content: 150], []}
]
@ -59,17 +60,17 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
@impl Provider
def build_tags(%{user: user}) do
with truncated_bio = scrub_html_and_truncate(user.bio || "") do
with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do
[
{:meta,
[
property: "og:title",
content: user_name_string(user)
content: Utils.user_name_string(user)
], []},
{:meta, [property: "og:url", content: User.profile_url(user)], []},
{:meta, [property: "og:description", content: truncated_bio], []},
{:meta, [property: "og:type", content: "website"], []},
{:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []},
{:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []},
{:meta, [property: "og:image:width", content: 150], []},
{:meta, [property: "og:image:height", content: 150], []}
]
@ -91,14 +92,15 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
case media_type do
"audio" ->
[
{:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []}
{:meta,
[property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}
| acc
]
"image" ->
[
{:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])],
[]},
{:meta,
[property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []},
{:meta, [property: "og:image:width", content: 150], []},
{:meta, [property: "og:image:height", content: 150], []}
| acc
@ -106,7 +108,8 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
"video" ->
[
{:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []}
{:meta,
[property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}
| acc
]
@ -118,37 +121,4 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
acc ++ rendered_tags
end)
end
defp scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
content
# html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.get_cached_stripped_html_for_object(object, __MODULE__)
|> Formatter.demojify()
|> Formatter.truncate()
end
defp scrub_html_and_truncate(content) when is_binary(content) do
content
# html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.strip_tags()
|> Formatter.demojify()
|> Formatter.truncate()
end
defp attachment_url(url) do
MediaProxy.url(url)
end
defp user_name_string(user) do
"#{user.name} " <>
if user.local do
"(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})"
else
"(@#{user.nickname})"
end
end
end

View file

@ -0,0 +1,21 @@
defmodule Pleroma.Web.Metadata.PlayerView do
use Pleroma.Web, :view
import Phoenix.HTML.Tag, only: [content_tag: 3, tag: 2]
def render("player.html", %{"mediaType" => type, "href" => href}) do
{tag_type, tag_attrs} =
case type do
"audio" <> _ -> {:audio, []}
"video" <> _ -> {:video, [loop: true]}
end
content_tag(
tag_type,
[
tag(:source, src: href, type: type),
"Your browser does not support #{type} playback."
],
[controls: true] ++ tag_attrs
)
end
end

View file

@ -3,44 +3,120 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
alias Pleroma.Web.Metadata.Providers.Provider
alias Pleroma.User
alias Pleroma.Web.Metadata
alias Pleroma.Web.Metadata.Providers.Provider
alias Pleroma.Web.Metadata.Utils
@behaviour Provider
@impl Provider
def build_tags(%{object: object}) do
if Metadata.activity_nsfw?(object) or object.data["attachment"] == [] do
build_tags(nil)
def build_tags(%{
activity_id: id,
object: object,
user: user
}) do
attachments = build_attachments(id, object)
scrubbed_content = Utils.scrub_html_and_truncate(object)
# Zero width space
content =
if scrubbed_content != "" and scrubbed_content != "\u200B" do
"" <> scrubbed_content <> ""
else
case find_first_acceptable_media_type(object) do
"image" ->
[{:meta, [property: "twitter:card", content: "summary_large_image"], []}]
"audio" ->
[{:meta, [property: "twitter:card", content: "player"], []}]
"video" ->
[{:meta, [property: "twitter:card", content: "player"], []}]
_ ->
build_tags(nil)
""
end
[
{:meta,
[
property: "twitter:title",
content: Utils.user_name_string(user)
], []},
{:meta,
[
property: "twitter:description",
content: content
], []}
] ++
if attachments == [] or Metadata.activity_nsfw?(object) do
[
{:meta,
[property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []},
{:meta, [property: "twitter:card", content: "summary_large_image"], []}
]
else
attachments
end
end
@impl Provider
def build_tags(_) do
[{:meta, [property: "twitter:card", content: "summary"], []}]
def build_tags(%{user: user}) do
with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do
[
{:meta,
[
property: "twitter:title",
content: Utils.user_name_string(user)
], []},
{:meta, [property: "twitter:description", content: truncated_bio], []},
{:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))],
[]},
{:meta, [property: "twitter:card", content: "summary"], []}
]
end
end
def find_first_acceptable_media_type(%{data: %{"attachment" => attachment}}) do
Enum.find_value(attachment, fn attachment ->
Enum.find_value(attachment["url"], fn url ->
defp build_attachments(id, %{data: %{"attachment" => attachments}}) do
Enum.reduce(attachments, [], fn attachment, acc ->
rendered_tags =
Enum.reduce(attachment["url"], [], fn url, acc ->
media_type =
Enum.find(["image", "audio", "video"], fn media_type ->
String.starts_with?(url["mediaType"], media_type)
end)
# TODO: Add additional properties to objects when we have the data available.
case media_type do
"audio" ->
[
{:meta, [property: "twitter:card", content: "player"], []},
{:meta, [property: "twitter:player:width", content: "480"], []},
{:meta, [property: "twitter:player:height", content: "80"], []},
{:meta, [property: "twitter:player", content: player_url(id)], []}
| acc
]
"image" ->
[
{:meta, [property: "twitter:card", content: "summary_large_image"], []},
{:meta,
[
property: "twitter:player",
content: Utils.attachment_url(url["href"])
], []}
| acc
]
# TODO: Need the true width and height values here or Twitter renders an iFrame with a bad aspect ratio
"video" ->
[
{:meta, [property: "twitter:card", content: "player"], []},
{:meta, [property: "twitter:player", content: player_url(id)], []},
{:meta, [property: "twitter:player:width", content: "480"], []},
{:meta, [property: "twitter:player:height", content: "480"], []}
| acc
]
_ ->
acc
end
end)
acc ++ rendered_tags
end)
end
defp player_url(id) do
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id)
end
end

View file

@ -0,0 +1,42 @@
# Pleroma: A lightweight social networking server
# Copyright \xc2\xa9 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Utils do
alias Pleroma.HTML
alias Pleroma.Formatter
alias Pleroma.Web.MediaProxy
def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
content
# html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.get_cached_stripped_html_for_object(object, __MODULE__)
|> Formatter.demojify()
|> Formatter.truncate()
end
def scrub_html_and_truncate(content) when is_binary(content) do
content
# html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.strip_tags()
|> Formatter.demojify()
|> Formatter.truncate()
end
def attachment_url(url) do
MediaProxy.url(url)
end
def user_name_string(user) do
"#{user.name} " <>
if user.local do
"(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})"
else
"(@#{user.nickname})"
end
end
end

View file

@ -5,10 +5,11 @@
defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
use Pleroma.Web, :controller
alias Pleroma.Stats
alias Pleroma.Web
alias Pleroma.{User, Repo}
alias Pleroma.Config
alias Pleroma.Repo
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.MRF
plug(Pleroma.Web.FederatingPlug)
@ -32,7 +33,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
# returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
# under software.
def raw_nodeinfo() do
def raw_nodeinfo do
instance = Application.get_env(:pleroma, :instance)
media_proxy = Application.get_env(:pleroma, :media_proxy)
suggestions = Application.get_env(:pleroma, :suggestions)
@ -44,6 +45,33 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
Application.get_env(:pleroma, :mrf_simple)
|> Enum.into(%{})
# This horror is needed to convert regex sigils to strings
mrf_keyword =
Application.get_env(:pleroma, :mrf_keyword, [])
|> Enum.map(fn {key, value} ->
{key,
Enum.map(value, fn
{pattern, replacement} ->
%{
"pattern" =>
if not is_binary(pattern) do
inspect(pattern)
else
pattern
end,
"replacement" => replacement
}
pattern ->
if not is_binary(pattern) do
inspect(pattern)
else
pattern
end
end)}
end)
|> Enum.into(%{})
mrf_policies =
MRF.get_policies()
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
@ -66,13 +94,12 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
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
if Keyword.get(instance, :mrf_transparency) do
%{
mrf_policies: mrf_policies,
mrf_simple: mrf_simple,
mrf_keyword: mrf_keyword,
mrf_user_allowlist: mrf_user_allowlist,
quarantined_instances: quarantined
}

20
lib/pleroma/web/oauth.ex Normal file
View file

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth do
def parse_scopes(scopes, _default) when is_list(scopes) do
Enum.filter(scopes, &(&1 not in [nil, ""]))
end
def parse_scopes(scopes, default) when is_binary(scopes) do
scopes
|> String.trim()
|> String.split(~r/[\s,]+/)
|> parse_scopes(default)
end
def parse_scopes(_, default) do
default
end
end

View file

@ -4,12 +4,12 @@
defmodule Pleroma.Web.OAuth.App do
use Ecto.Schema
import Ecto.{Changeset}
import Ecto.Changeset
schema "apps" do
field(:client_name, :string)
field(:redirect_uris, :string)
field(:scopes, :string)
field(:scopes, {:array, :string}, default: [])
field(:website, :string)
field(:client_id, :string)
field(:client_secret, :string)
@ -25,8 +25,14 @@ defmodule Pleroma.Web.OAuth.App do
if changeset.valid? do
changeset
|> put_change(:client_id, :crypto.strong_rand_bytes(32) |> Base.url_encode64())
|> put_change(:client_secret, :crypto.strong_rand_bytes(32) |> Base.url_encode64())
|> put_change(
:client_id,
:crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
)
|> put_change(
:client_secret,
:crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
)
else
changeset
end

View file

@ -5,13 +5,17 @@
defmodule Pleroma.Web.OAuth.Authorization do
use Ecto.Schema
alias Pleroma.{User, Repo}
alias Pleroma.Web.OAuth.{Authorization, App}
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.App
import Ecto.{Changeset, Query}
import Ecto.Changeset
import Ecto.Query
schema "oauth_authorizations" do
field(:token, :string)
field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime)
field(:used, :boolean, default: false)
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
@ -20,14 +24,16 @@ defmodule Pleroma.Web.OAuth.Authorization do
timestamps()
end
def create_authorization(%App{} = app, %User{} = user) do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do
scopes = scopes || app.scopes
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
authorization = %Authorization{
token: token,
used: false,
user_id: user.id,
app_id: app.id,
scopes: scopes,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
}

View file

@ -5,42 +5,56 @@
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Pleroma.Web.OAuth.{Authorization, Token, App}
alias Pleroma.{Repo, User}
alias Pleroma.Web.Auth.Authenticator
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.App
alias Pleroma.Repo
alias Pleroma.User
alias Comeonin.Pbkdf2
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
plug(:fetch_session)
plug(:fetch_flash)
action_fallback(Pleroma.Web.OAuth.FallbackController)
def authorize(conn, params) do
render(conn, "show.html", %{
app = Repo.get_by(App, client_id: params["client_id"])
available_scopes = (app && app.scopes) || []
scopes = oauth_scopes(params, nil) || available_scopes
render(conn, Authenticator.auth_template(), %{
response_type: params["response_type"],
client_id: params["client_id"],
scope: params["scope"],
available_scopes: available_scopes,
scopes: scopes,
redirect_uri: params["redirect_uri"],
state: params["state"]
state: params["state"],
params: params
})
end
def create_authorization(conn, %{
"authorization" =>
%{
"name" => name,
"password" => password,
"client_id" => client_id,
"redirect_uri" => redirect_uri
} = params
} = auth_params
}) do
with %User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)},
%App{} = app <- Repo.get_by(App, client_id: client_id),
{:ok, auth} <- Authorization.create_authorization(app, user) do
# Special case: Local MastodonFE.
true <- redirect_uri in String.split(app.redirect_uris),
scopes <- oauth_scopes(auth_params, []),
{:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes},
# Note: `scope` param is intentionally not optional in this context
{:missing_scopes, false} <- {:missing_scopes, scopes == []},
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
redirect_uri =
if redirect_uri == "." do
# Special case: Local MastodonFE
mastodon_api_url(conn, :login)
else
redirect_uri
@ -58,8 +72,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
url_params = %{:code => auth.token}
url_params =
if params["state"] do
Map.put(url_params, :state, params["state"])
if auth_params["state"] do
Map.put(url_params, :state, auth_params["state"])
else
url_params
end
@ -69,19 +83,23 @@ defmodule Pleroma.Web.OAuth.OAuthController do
redirect(conn, external: url)
end
else
{scopes_issue, _} when scopes_issue in [:unsupported_scopes, :missing_scopes] ->
conn
|> put_flash(:error, "Permissions not specified.")
|> put_status(:unauthorized)
|> authorize(auth_params)
{:auth_active, false} ->
conn
|> put_flash(:error, "Account confirmation pending")
|> put_flash(:error, "Account confirmation pending.")
|> put_status(:forbidden)
|> authorize(params)
|> authorize(auth_params)
error ->
error
Authenticator.handle_error(conn, error)
end
end
# TODO
# - proper scope handling
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
with %App{} = app <- get_app_from_request(conn, params),
fixed_token = fix_padding(params["code"]),
@ -95,7 +113,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
refresh_token: token.refresh_token,
created_at: DateTime.to_unix(inserted_at),
expires_in: 60 * 10,
scope: "read write follow"
scope: Enum.join(token.scopes, " ")
}
json(conn, response)
@ -106,8 +124,6 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
# TODO
# - investigate a way to verify the user wants to grant read/write/follow once scope handling is done
def token_exchange(
conn,
%{"grant_type" => "password", "username" => name, "password" => password} = params
@ -116,14 +132,17 @@ defmodule Pleroma.Web.OAuth.OAuthController do
%User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:ok, auth} <- Authorization.create_authorization(app, user),
scopes <- oauth_scopes(params, app.scopes),
[] <- scopes -- app.scopes,
true <- Enum.any?(scopes),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do
response = %{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: 60 * 10,
scope: "read write follow"
scope: Enum.join(token.scopes, " ")
}
json(conn, response)
@ -169,7 +188,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
token
|> URI.decode()
|> Base.url_decode64!(padding: false)
|> Base.url_encode64()
|> Base.url_encode64(padding: false)
end
defp get_app_from_request(conn, params) do

View file

@ -7,12 +7,16 @@ defmodule Pleroma.Web.OAuth.Token do
import Ecto.Query
alias Pleroma.{User, Repo}
alias Pleroma.Web.OAuth.{Token, App, Authorization}
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
schema "oauth_tokens" do
field(:token, :string)
field(:refresh_token, :string)
field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime)
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
belongs_to(:app, App)
@ -23,17 +27,19 @@ defmodule Pleroma.Web.OAuth.Token do
def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do
create_token(app, Repo.get(User, auth.user_id))
create_token(app, Repo.get(User, auth.user_id), auth.scopes)
end
end
def create_token(%App{} = app, %User{} = user) do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
def create_token(%App{} = app, %User{} = user, scopes \\ nil) do
scopes = scopes || app.scopes
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
token = %Token{
token: token,
refresh_token: refresh_token,
scopes: scopes,
user_id: user.id,
app_id: app.id,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
@ -44,9 +50,27 @@ defmodule Pleroma.Web.OAuth.Token do
def delete_user_tokens(%User{id: user_id}) do
from(
t in Pleroma.Web.OAuth.Token,
t in Token,
where: t.user_id == ^user_id
)
|> Repo.delete_all()
end
def delete_user_token(%User{id: user_id}, token_id) do
from(
t in Token,
where: t.user_id == ^user_id,
where: t.id == ^token_id
)
|> Repo.delete_all()
end
def get_user_tokens(%User{id: user_id}) do
from(
t in Token,
where: t.user_id == ^user_id
)
|> Repo.all()
|> Repo.preload(:app)
end
end

View file

@ -3,8 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.ActivityRepresenter do
alias Pleroma.{Activity, User, Object}
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Object
alias Pleroma.Web.OStatus.UserRepresenter
require Logger
defp get_href(id) do

View file

@ -3,10 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.FeedRepresenter do
alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.{UserRepresenter, ActivityRepresenter}
alias Pleroma.User
alias Pleroma.Web.OStatus
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.OStatus.UserRepresenter
def to_simple_form(user, activities, _users) do
most_recent_update =

View file

@ -3,7 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.FollowHandler do
alias Pleroma.Web.{XML, OStatus}
alias Pleroma.Web.XML
alias Pleroma.Web.OStatus
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.User

View file

@ -4,8 +4,10 @@
defmodule Pleroma.Web.OStatus.NoteHandler do
require Logger
alias Pleroma.Web.{XML, OStatus}
alias Pleroma.{Object, Activity}
alias Pleroma.Web.OStatus
alias Pleroma.Web.XML
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI

View file

@ -3,7 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.UnfollowHandler do
alias Pleroma.Web.{XML, OStatus}
alias Pleroma.Web.XML
alias Pleroma.Web.OStatus
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.User

View file

@ -9,11 +9,19 @@ defmodule Pleroma.Web.OStatus do
import Pleroma.Web.XML
require Logger
alias Pleroma.{Repo, User, Web, Object, Activity}
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Object
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler}
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub
alias Pleroma.Web.OStatus.FollowHandler
alias Pleroma.Web.OStatus.UnfollowHandler
alias Pleroma.Web.OStatus.NoteHandler
alias Pleroma.Web.OStatus.DeleteHandler
def is_representable?(%Activity{data: data}) do
object = Object.normalize(data["object"])

View file

@ -5,13 +5,18 @@
defmodule Pleroma.Web.OStatus.OStatusController do
use Pleroma.Web, :controller
alias Pleroma.{User, Activity, Object}
alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter}
alias Pleroma.Web.{OStatus, Federator}
alias Pleroma.Web.XML
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.OStatus.FeedRepresenter
alias Pleroma.Web.Federator
alias Pleroma.Web.OStatus
alias Pleroma.Web.XML
plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
@ -29,6 +34,9 @@ defmodule Pleroma.Web.OStatus.OStatusController do
"activity+json" ->
ActivityPubController.call(conn, :user)
"json" ->
ActivityPubController.call(conn, :user)
_ ->
with %User{} = user <- User.get_cached_by_nickname(nickname) do
redirect(conn, external: OStatus.feed_path(user))
@ -83,19 +91,19 @@ defmodule Pleroma.Web.OStatus.OStatusController do
{:ok, body, _conn} = read_body(conn)
{:ok, doc} = decode_or_retry(body)
Federator.enqueue(:incoming_doc, doc)
Federator.incoming_doc(doc)
conn
|> send_resp(200, "")
end
def object(conn, %{"uuid" => uuid}) do
if get_format(conn) == "activity+json" do
if get_format(conn) in ["activity+json", "json"] do
ActivityPubController.call(conn, :object)
else
with id <- o_status_url(conn, :object, uuid),
{_, %Activity{} = activity} <- {:activity, Activity.get_create_by_object_ap_id(id)},
{_, true} <- {:public?, ActivityPub.is_public?(activity)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do
"html" -> redirect(conn, to: "/notice/#{activity.id}")
@ -115,12 +123,12 @@ defmodule Pleroma.Web.OStatus.OStatusController do
end
def activity(conn, %{"uuid" => uuid}) do
if get_format(conn) == "activity+json" do
if get_format(conn) in ["activity+json", "json"] do
ActivityPubController.call(conn, :activity)
else
with id <- o_status_url(conn, :activity, uuid),
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
{_, true} <- {:public?, ActivityPub.is_public?(activity)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format = get_format(conn) do
"html" -> redirect(conn, to: "/notice/#{activity.id}")
@ -141,7 +149,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
def notice(conn, %{"id" => id}) do
with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(id)},
{_, true} <- {:public?, ActivityPub.is_public?(activity)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format = get_format(conn) do
"html" ->
@ -149,6 +157,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
%Object{} = object = Object.normalize(activity.data["object"])
Fallback.RedirectController.redirector_with_meta(conn, %{
activity_id: activity.id,
object: object,
url:
Pleroma.Web.Router.Helpers.o_status_url(
@ -180,6 +189,30 @@ defmodule Pleroma.Web.OStatus.OStatusController do
end
end
# Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
def notice_player(conn, %{"id" => id}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
true <- Visibility.is_public?(activity),
%Object{} = object <- Object.normalize(activity.data["object"]),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
conn
|> put_layout(:metadata_player)
|> put_resp_header("x-frame-options", "ALLOW")
|> put_resp_header(
"content-security-policy",
"default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;"
)
|> put_view(Pleroma.Web.Metadata.PlayerView)
|> render("player.html", url)
else
_error ->
conn
|> put_status(404)
|> Fallback.RedirectController.redirector(nil, 404)
end
end
defp represent_activity(
conn,
"activity+json",

View file

@ -5,7 +5,8 @@
defmodule Pleroma.Web.Push do
use GenServer
alias Pleroma.{Repo, User}
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Push.Subscription
require Logger

View file

@ -4,8 +4,11 @@
defmodule Pleroma.Web.Push.Subscription do
use Ecto.Schema
import Ecto.Changeset
alias Pleroma.{Repo, User}
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Push.Subscription

View file

@ -3,7 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Helpers do
alias Pleroma.{Activity, Object, HTML}
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.HTML
alias Pleroma.Web.RichMedia.Parser
def fetch_data_for_activity(%Activity{} = activity) do

View file

@ -9,6 +9,13 @@ defmodule Pleroma.Web.RichMedia.Parser do
Pleroma.Web.RichMedia.Parsers.OEmbed
]
@hackney_options [
pool: :media,
timeout: 2_000,
recv_timeout: 2_000,
max_body: 2_000_000
]
def parse(nil), do: {:error, "No URL provided"}
if Mix.env() == :test do
@ -28,7 +35,7 @@ defmodule Pleroma.Web.RichMedia.Parser do
defp parse_url(url) do
try do
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: [pool: :media])
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options)
html |> maybe_parse() |> clean_parsed_data() |> check_parsed_data()
rescue

View file

@ -74,6 +74,29 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
pipeline :oauth_read_or_unauthenticated do
plug(Pleroma.Plugs.OAuthScopesPlug, %{
scopes: ["read"],
fallback: :proceed_unauthenticated
})
end
pipeline :oauth_read do
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]})
end
pipeline :oauth_write do
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["write"]})
end
pipeline :oauth_follow do
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["follow"]})
end
pipeline :oauth_push do
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
end
pipeline :well_known do
plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
end
@ -101,6 +124,7 @@ defmodule Pleroma.Web.Router do
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api)
get("/password_reset/:token", UtilController, :show_password_reset)
post("/password_reset", UtilController, :password_reset)
get("/emoji", UtilController, :emoji)
@ -113,8 +137,11 @@ defmodule Pleroma.Web.Router do
end
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
pipe_through([:admin_api, :oauth_write])
get("/users", AdminAPIController, :list_users)
delete("/user", AdminAPIController, :user_delete)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
post("/user", AdminAPIController, :user_create)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
@ -124,6 +151,8 @@ defmodule Pleroma.Web.Router do
post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add)
delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete)
put("/activation_status/:nickname", AdminAPIController, :set_activation_status)
post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)
@ -135,19 +164,34 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
get("/ostatus_subscribe", UtilController, :remote_follow)
post("/ostatus_subscribe", UtilController, :do_remote_follow)
post("/main/ostatus", UtilController, :remote_subscribe)
get("/ostatus_subscribe", UtilController, :remote_follow)
scope [] do
pipe_through(:oauth_follow)
post("/ostatus_subscribe", UtilController, :do_remote_follow)
end
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:authenticated_api)
post("/blocks_import", UtilController, :blocks_import)
post("/follow_import", UtilController, :follow_import)
scope [] do
pipe_through(:oauth_write)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
end
scope [] do
pipe_through(:oauth_follow)
post("/blocks_import", UtilController, :blocks_import)
post("/follow_import", UtilController, :follow_import)
end
end
scope "/oauth", Pleroma.Web.OAuth do
get("/authorize", OAuthController, :authorize)
post("/authorize", OAuthController, :create_authorization)
@ -158,35 +202,51 @@ defmodule Pleroma.Web.Router do
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
scope [] do
pipe_through(:oauth_read)
get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
get("/accounts/relationships", MastodonAPIController, :relationships)
get("/accounts/search", MastodonAPIController, :account_search)
post("/accounts/:id/follow", MastodonAPIController, :follow)
post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
post("/accounts/:id/block", MastodonAPIController, :block)
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)
post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
post("/follows", MastodonAPIController, :follow)
get("/blocks", MastodonAPIController, :blocks)
get("/mutes", MastodonAPIController, :empty_array)
get("/mutes", MastodonAPIController, :mutes)
get("/timelines/home", MastodonAPIController, :home_timeline)
get("/timelines/direct", MastodonAPIController, :dm_timeline)
get("/favourites", MastodonAPIController, :favourites)
get("/bookmarks", MastodonAPIController, :bookmarks)
post("/notifications/clear", MastodonAPIController, :clear_notifications)
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
get("/notifications", MastodonAPIController, :notifications)
get("/notifications/:id", MastodonAPIController, :get_notification)
get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list)
get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
get("/domain_blocks", MastodonAPIController, :domain_blocks)
get("/filters", MastodonAPIController, :get_filters)
get("/suggestions", MastodonAPIController, :suggestions)
get("/endorsements", MastodonAPIController, :empty_array)
get("/pleroma/flavour", MastodonAPIController, :get_flavour)
end
scope [] do
pipe_through(:oauth_write)
patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
post("/statuses", MastodonAPIController, :post_status)
delete("/statuses/:id", MastodonAPIController, :delete_status)
@ -198,79 +258,100 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status)
post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status)
post("/notifications/clear", MastodonAPIController, :clear_notifications)
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
get("/notifications", MastodonAPIController, :notifications)
get("/notifications/:id", MastodonAPIController, :get_notification)
post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
post("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media)
get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list)
delete("/lists/:id", MastodonAPIController, :delete_list)
post("/lists", MastodonAPIController, :create_list)
put("/lists/:id", MastodonAPIController, :rename_list)
get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
post("/lists/:id/accounts", MastodonAPIController, :add_to_list)
delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list)
get("/domain_blocks", MastodonAPIController, :domain_blocks)
post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
get("/filters", MastodonAPIController, :get_filters)
post("/filters", MastodonAPIController, :create_filter)
get("/filters/:id", MastodonAPIController, :get_filter)
put("/filters/:id", MastodonAPIController, :update_filter)
delete("/filters/:id", MastodonAPIController, :delete_filter)
post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour)
post("/reports", MastodonAPIController, :reports)
end
scope [] do
pipe_through(:oauth_follow)
post("/follows", MastodonAPIController, :follow)
post("/accounts/:id/follow", MastodonAPIController, :follow)
post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
post("/accounts/:id/block", MastodonAPIController, :block)
post("/accounts/:id/unblock", MastodonAPIController, :unblock)
post("/accounts/:id/mute", MastodonAPIController, :mute)
post("/accounts/:id/unmute", MastodonAPIController, :unmute)
post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
end
scope [] do
pipe_through(:oauth_push)
post("/push/subscription", MastodonAPIController, :create_push_subscription)
get("/push/subscription", MastodonAPIController, :get_push_subscription)
put("/push/subscription", MastodonAPIController, :update_push_subscription)
delete("/push/subscription", MastodonAPIController, :delete_push_subscription)
get("/suggestions", MastodonAPIController, :suggestions)
get("/endorsements", MastodonAPIController, :empty_array)
end
end
scope "/api/web", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
pipe_through([:authenticated_api, :oauth_write])
put("/settings", MastodonAPIController, :put_settings)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/instance", MastodonAPIController, :masto_instance)
get("/instance/peers", MastodonAPIController, :peers)
post("/apps", MastodonAPIController, :create_app)
get("/custom_emojis", MastodonAPIController, :custom_emojis)
get("/statuses/:id/card", MastodonAPIController, :status_card)
get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
get("/trends", MastodonAPIController, :empty_array)
scope [] do
pipe_through(:oauth_read_or_unauthenticated)
get("/timelines/public", MastodonAPIController, :public_timeline)
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
get("/statuses/:id", MastodonAPIController, :get_status)
get("/statuses/:id/context", MastodonAPIController, :get_context)
get("/statuses/:id/card", MastodonAPIController, :status_card)
get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
get("/accounts/:id/followers", MastodonAPIController, :followers)
get("/accounts/:id/following", MastodonAPIController, :following)
get("/accounts/:id", MastodonAPIController, :user)
get("/trends", MastodonAPIController, :empty_array)
get("/search", MastodonAPIController, :search)
end
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
pipe_through([:api, :oauth_read_or_unauthenticated])
get("/search", MastodonAPIController, :search2)
end
@ -287,6 +368,21 @@ defmodule Pleroma.Web.Router do
scope "/api", Pleroma.Web do
pipe_through(:api)
post("/account/register", TwitterAPI.Controller, :register)
post("/account/password_reset", TwitterAPI.Controller, :password_reset)
post("/account/resend_confirmation_email", TwitterAPI.Controller, :resend_confirmation_email)
get(
"/account/confirm_email/:user_id/:token",
TwitterAPI.Controller,
:confirm_email,
as: :confirm_email
)
scope [] do
pipe_through(:oauth_read_or_unauthenticated)
get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/users/show", TwitterAPI.Controller, :show_user)
@ -297,24 +393,13 @@ defmodule Pleroma.Web.Router do
get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
post("/account/register", TwitterAPI.Controller, :register)
post("/account/password_reset", TwitterAPI.Controller, :password_reset)
get(
"/account/confirm_email/:user_id/:token",
TwitterAPI.Controller,
:confirm_email,
as: :confirm_email
)
post("/account/resend_confirmation_email", TwitterAPI.Controller, :resend_confirmation_email)
get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
end
end
scope "/api", Pleroma.Web do
pipe_through(:api)
pipe_through([:api, :oauth_read_or_unauthenticated])
get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline)
@ -328,20 +413,22 @@ defmodule Pleroma.Web.Router do
end
scope "/api", Pleroma.Web, as: :twitter_api_search do
pipe_through(:api)
pipe_through([:api, :oauth_read_or_unauthenticated])
get("/pleroma/search_user", TwitterAPI.Controller, :search_user)
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)
get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
scope [] do
pipe_through(:oauth_read)
get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
post("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
post("/account/update_profile", TwitterAPI.Controller, :update_profile)
post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
@ -349,9 +436,25 @@ defmodule Pleroma.Web.Router do
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.
get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
get("/friends/ids", TwitterAPI.Controller, :friends_ids)
get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
end
scope [] do
pipe_through(:oauth_write)
post("/account/update_profile", TwitterAPI.Controller, :update_profile)
post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
post("/statuses/update", TwitterAPI.Controller, :status_update)
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
@ -361,15 +464,6 @@ defmodule Pleroma.Web.Router do
post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)
get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
post("/friendships/create", TwitterAPI.Controller, :follow)
post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
post("/blocks/create", TwitterAPI.Controller, :block)
post("/blocks/destroy", TwitterAPI.Controller, :unblock)
post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
post("/media/upload", TwitterAPI.Controller, :upload_json)
post("/media/metadata/create", TwitterAPI.Controller, :update_media)
@ -379,22 +473,28 @@ defmodule Pleroma.Web.Router do
post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
end
get("/friends/ids", TwitterAPI.Controller, :friends_ids)
get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
scope [] do
pipe_through(:oauth_follow)
get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
post("/friendships/create", TwitterAPI.Controller, :follow)
post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
post("/blocks/create", TwitterAPI.Controller, :block)
post("/blocks/destroy", TwitterAPI.Controller, :unblock)
end
end
pipeline :ap_relay do
plug(:accepts, ["activity+json"])
plug(:accepts, ["activity+json", "json"])
end
pipeline :ostatus do
plug(:accepts, ["html", "xml", "atom", "activity+json"])
plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
end
pipeline :oembed do
@ -407,6 +507,7 @@ defmodule Pleroma.Web.Router do
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
get("/users/:nickname/feed", OStatus.OStatusController, :feed)
get("/users/:nickname", OStatus.OStatusController, :feed_redirect)
@ -423,7 +524,7 @@ defmodule Pleroma.Web.Router do
end
pipeline :activitypub do
plug(:accepts, ["activity+json"])
plug(:accepts, ["activity+json", "json"])
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
end
@ -438,7 +539,7 @@ defmodule Pleroma.Web.Router do
end
pipeline :activitypub_client do
plug(:accepts, ["activity+json"])
plug(:accepts, ["activity+json", "json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
@ -454,10 +555,17 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_client])
scope [] do
pipe_through(:oauth_read)
get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
end
scope [] do
pipe_through(:oauth_write)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
end
end
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_relay)
@ -466,8 +574,8 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
@ -486,9 +594,12 @@ defmodule Pleroma.Web.Router do
pipe_through(:mastodon_html)
get("/web/login", MastodonAPIController, :login)
post("/web/login", MastodonAPIController, :login_post)
get("/web/*path", MastodonAPIController, :index)
delete("/auth/sign_out", MastodonAPIController, :logout)
scope [] do
pipe_through(:oauth_read_or_unauthenticated)
get("/web/*path", MastodonAPIController, :index)
end
end
pipeline :remote_media do
@ -496,6 +607,7 @@ defmodule Pleroma.Web.Router do
scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end

View file

@ -6,10 +6,12 @@ defmodule Pleroma.Web.Salmon do
@httpoison Application.get_env(:pleroma, :httpoison)
use Bitwise
alias Pleroma.Instances
alias Pleroma.User
alias Pleroma.Web.XML
alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.User
require Logger
def decode(salmon) do
@ -227,7 +229,7 @@ defmodule Pleroma.Web.Salmon do
|> Enum.each(fn remote_user ->
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
Pleroma.Web.Federator.enqueue(:publish_single_salmon, %{
Pleroma.Web.Federator.publish_single_salmon(%{
recipient: remote_user,
feed: feed,
poster: poster,

View file

@ -5,8 +5,12 @@
defmodule Pleroma.Web.Streamer do
use GenServer
require Logger
alias Pleroma.{User, Notification, Activity, Object, Repo}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.User
alias Pleroma.Notification
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.Visibility
@keepalive_interval :timer.seconds(30)
@ -69,7 +73,7 @@ defmodule Pleroma.Web.Streamer do
def handle_cast(%{action: :stream, topic: "list", item: item}, topics) do
# filter the recipient list if the activity is not public, see #270.
recipient_lists =
case ActivityPub.is_public?(item) do
case Visibility.is_public?(item) do
true ->
Pleroma.List.get_lists_from_activity(item)
@ -78,7 +82,7 @@ defmodule Pleroma.Web.Streamer do
|> Enum.filter(fn list ->
owner = Repo.get(User, list.user_id)
ActivityPub.visible_for_user?(item, owner)
Visibility.visible_for_user?(item, owner)
end)
end

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