Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into develop

This commit is contained in:
sadposter 2019-05-16 20:24:43 +01:00
commit c0b7bc2927
96 changed files with 2827 additions and 969 deletions

View file

@ -52,6 +52,7 @@ unit-testing:
- mix ecto.create - mix ecto.create
- mix ecto.migrate - mix ecto.migrate
- mix test --trace --preload-modules - mix test --trace --preload-modules
- mix coveralls
lint: lint:
stage: test stage: test

View file

@ -10,7 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc. - A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc.
- [Prometheus](https://prometheus.io/) metrics - [Prometheus](https://prometheus.io/) metrics
- Support for Mastodon's remote interaction - Support for Mastodon's remote interaction
- Mix Tasks: `mix pleroma.database bump_all_conversations`
- Mix Tasks: `mix pleroma.database remove_embedded_objects` - Mix Tasks: `mix pleroma.database remove_embedded_objects`
- Mix Tasks: `mix pleroma.user toggle_confirmed`
- Federation: Support for reports - Federation: Support for reports
- Configuration: `safe_dm_mentions` option - Configuration: `safe_dm_mentions` option
- Configuration: `link_name` option - Configuration: `link_name` option
@ -22,12 +24,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Admin API: Endpoints for listing/revoking invite tokens - Admin API: Endpoints for listing/revoking invite tokens
- Admin API: Endpoints for making users follow/unfollow each other - Admin API: Endpoints for making users follow/unfollow each other
- Admin API: added filters (role, tags, email, name) for users endpoint - Admin API: added filters (role, tags, email, name) for users endpoint
- Admin API: Endpoints for managing reports
- Admin API: Endpoints for deleting and changing the scope of individual reported statuses
- AdminFE: initial release with basic user management accessible at /pleroma/admin/
- Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/) - Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/)
- Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension) - Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension)
- Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension) - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
- Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/) - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
- Mastodon API: `POST /api/v1/accounts` (account creation API)
- ActivityPub C2S: OAuth endpoints - ActivityPub C2S: OAuth endpoints
- Metadata RelMe provider - Metadata: RelMe provider
- OAuth: added support for refresh tokens - OAuth: added support for refresh tokens
- Emoji packs and emoji pack manager - Emoji packs and emoji pack manager
@ -42,8 +48,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Federation: Removed `inReplyToStatusId` from objects - Federation: Removed `inReplyToStatusId` from objects
- Configuration: Dedupe enabled by default - Configuration: Dedupe enabled by default
- Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work. - Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work.
- Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change
- Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats. - Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats.
- Admin API: Move the user related API to `api/pleroma/admin/users`
- Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change
- Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications` - Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications`
- Mastodon API: Add `languages` and `registrations` to `/api/v1/instance` - Mastodon API: Add `languages` and `registrations` to `/api/v1/instance`
- Mastodon API: Provide plaintext versions of cw/content in the Status entity - Mastodon API: Provide plaintext versions of cw/content in the Status entity
@ -57,18 +64,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Add `with_muted` parameter to timeline endpoints - Mastodon API: Add `with_muted` parameter to timeline endpoints
- Mastodon API: Actual reblog hiding instead of a dummy - Mastodon API: Actual reblog hiding instead of a dummy
- Mastodon API: Remove attachment limit in the Status entity - Mastodon API: Remove attachment limit in the Status entity
- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints.
- Deps: Updated Cowboy to 2.6 - Deps: Updated Cowboy to 2.6
- Deps: Updated Ecto to 3.0.7 - Deps: Updated Ecto to 3.0.7
- Don't ship finmoji by default, they can be installed as an emoji pack - Don't ship finmoji by default, they can be installed as an emoji pack
- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints. - Hide deactivated users and their statuses
- Admin API: Move the user related API to `api/pleroma/admin/users`
### Fixed ### Fixed
- Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended. - Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended.
- Followers counter not being updated when a follower is blocked - Followers counter not being updated when a follower is blocked
- Deactivated users being able to request an access token - Deactivated users being able to request an access token
- Limit on request body in rich media/relme parsers being ignored resulting in a possible memory leak - Limit on request body in rich media/relme parsers being ignored resulting in a possible memory leak
- proper Twitter Card generation instead of a dummy - Proper Twitter Card generation instead of a dummy
- Deletions failing for users with a large number of posts - Deletions failing for users with a large number of posts
- NodeInfo: Include admins in `staffAccounts` - NodeInfo: Include admins in `staffAccounts`
- ActivityPub: Crashing when requesting empty local user's outbox - ActivityPub: Crashing when requesting empty local user's outbox
@ -92,6 +99,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Handling of `reblogs` in `/api/v1/accounts/:id/follow` - Mastodon API: Handling of `reblogs` in `/api/v1/accounts/:id/follow`
- Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON - Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON
- Mastodon API: Exposing default scope of the user to anyone - Mastodon API: Exposing default scope of the user to anyone
- Mastodon API: Make `irreversible` field default to `false` [`POST /api/v1/filters`]
## Removed
- Configuration: `config :pleroma, :fe` in favor of the more flexible `config :pleroma, :frontend_configurations`
## [0.9.9999] - 2019-04-05 ## [0.9.9999] - 2019-04-05
### Security ### Security

View file

@ -15,6 +15,14 @@ priv/static/images/pleroma-tan.png
--- ---
The following files are copyright © 2019 shitposter.club, and are distributed
under the Creative Commons Attribution 4.0 International license, you should
have received a copy of the license file as CC-BY-4.0.
priv/static/images/pleroma-fox-tan-shy.png
---
The following files are copyright © 2017-2019 Pleroma Authors The following files are copyright © 2017-2019 Pleroma Authors
<https://pleroma.social/>, and are distributed under the Creative Commons <https://pleroma.social/>, and are distributed under the Creative Commons
Attribution-ShareAlike 4.0 International license, you should have received Attribution-ShareAlike 4.0 International license, you should have received

View file

@ -212,6 +212,11 @@
registrations_open: true, registrations_open: true,
federating: true, federating: true,
federation_reachability_timeout_days: 7, federation_reachability_timeout_days: 7,
federation_publisher_modules: [
Pleroma.Web.ActivityPub.Publisher,
Pleroma.Web.Websub,
Pleroma.Web.Salmon
],
allow_relay: true, allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true, public: true,
@ -234,6 +239,8 @@
safe_dm_mentions: false, safe_dm_mentions: false,
healthcheck: false healthcheck: false
config :pleroma, :app_account_creation, enabled: true, max_requests: 25, interval: 1800
config :pleroma, :markup, config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because # XXX - unfortunately, inline images must be enabled by default right now, because
# of custom emoji. Issue #275 discusses defanging that somehow. # of custom emoji. Issue #275 discusses defanging that somehow.
@ -246,25 +253,6 @@
Pleroma.HTML.Scrubber.Default Pleroma.HTML.Scrubber.Default
] ]
# Deprecated, will be gone in 1.0
config :pleroma, :fe,
theme: "pleroma-dark",
logo: "/static/logo.png",
logo_mask: true,
logo_margin: "0.1em",
background: "/static/aurora_borealis.jpg",
redirect_root_no_login: "/main/all",
redirect_root_login: "/main/friends",
show_instance_panel: true,
scope_options_enabled: false,
formatting_options_enabled: false,
collapse_message_with_subject: false,
hide_post_stats: false,
hide_user_stats: false,
scope_copy: true,
subject_line_behavior: "email",
always_show_subject_input: true
config :pleroma, :frontend_configurations, config :pleroma, :frontend_configurations,
pleroma_fe: %{ pleroma_fe: %{
theme: "pleroma-dark", theme: "pleroma-dark",
@ -476,6 +464,9 @@
token_expires_in: 600, token_expires_in: 600,
issue_new_refresh_token: true issue_new_refresh_token: true
config :http_signatures,
adapter: Pleroma.Signature
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View file

@ -59,6 +59,8 @@
total_user_limit: 3, total_user_limit: 3,
enabled: false enabled: false
config :pleroma, :app_account_creation, max_requests: 5
try do try do
import_config "test.secret.exs" import_config "test.secret.exs"
rescue rescue

View file

@ -24,7 +24,7 @@ Authentication is required and the user must be an admin.
- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com` - Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com`
- Response: - Response:
```JSON ```json
{ {
"page_size": integer, "page_size": integer,
"count": integer, "count": integer,
@ -92,7 +92,7 @@ Authentication is required and the user must be an admin.
- `nickname` - `nickname`
- Response: Users object - Response: Users object
```JSON ```json
{ {
"deactivated": bool, "deactivated": bool,
"id": integer, "id": integer,
@ -106,15 +106,15 @@ Authentication is required and the user must be an admin.
- Method: `PUT` - Method: `PUT`
- Params: - Params:
- `nickname` - `nicknames` (array)
- `tags` - `tags` (array)
### Untag a list of users ### Untag a list of users
- Method: `DELETE` - Method: `DELETE`
- Params: - Params:
- `nickname` - `nicknames` (array)
- `tags` - `tags` (array)
## `/api/pleroma/admin/users/:nickname/permission_group` ## `/api/pleroma/admin/users/:nickname/permission_group`
@ -124,7 +124,7 @@ Authentication is required and the user must be an admin.
- Params: none - Params: none
- Response: - Response:
```JSON ```json
{ {
"is_moderator": bool, "is_moderator": bool,
"is_admin": bool "is_admin": bool
@ -141,7 +141,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Params: none - Params: none
- Response: - Response:
```JSON ```json
{ {
"is_moderator": bool, "is_moderator": bool,
"is_admin": bool "is_admin": bool
@ -223,7 +223,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Params: none - Params: none
- Response: - Response:
```JSON ```json
{ {
"invites": [ "invites": [
@ -250,7 +250,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `token` - `token`
- Response: - Response:
```JSON ```json
{ {
"id": integer, "id": integer,
"token": string, "token": string,
@ -280,3 +280,280 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Methods: `GET` - Methods: `GET`
- Params: none - Params: none
- Response: password reset token (base64 string) - Response: password reset token (base64 string)
## `/api/pleroma/admin/reports`
### Get a list of reports
- Method `GET`
- Params:
- `state`: optional, the state of reports. Valid values are `open`, `closed` and `resolved`
- `limit`: optional, the number of records to retrieve
- `since_id`: optional, returns results that are more recent than the specified id
- `max_id`: optional, returns results that are older than the specified id
- Response:
- On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin
- On success: JSON, returns a list of reports, where:
- `account`: the user who has been reported
- `actor`: the user who has sent the report
- `statuses`: list of statuses that have been included to the report
```json
{
"reports": [
{
"account": {
"acct": "user",
"avatar": "https://pleroma.example.org/images/avi.png",
"avatar_static": "https://pleroma.example.org/images/avi.png",
"bot": false,
"created_at": "2019-04-23T17:32:04.000Z",
"display_name": "User",
"emojis": [],
"fields": [],
"followers_count": 1,
"following_count": 1,
"header": "https://pleroma.example.org/images/banner.png",
"header_static": "https://pleroma.example.org/images/banner.png",
"id": "9i6dAJqSGSKMzLG2Lo",
"locked": false,
"note": "",
"pleroma": {
"confirmation_pending": false,
"hide_favorites": true,
"hide_followers": false,
"hide_follows": false,
"is_admin": false,
"is_moderator": false,
"relationship": {},
"tags": []
},
"source": {
"note": "",
"pleroma": {},
"sensitive": false
},
"statuses_count": 3,
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
"actor": {
"acct": "lain",
"avatar": "https://pleroma.example.org/images/avi.png",
"avatar_static": "https://pleroma.example.org/images/avi.png",
"bot": false,
"created_at": "2019-03-28T17:36:03.000Z",
"display_name": "Roger Braun",
"emojis": [],
"fields": [],
"followers_count": 1,
"following_count": 1,
"header": "https://pleroma.example.org/images/banner.png",
"header_static": "https://pleroma.example.org/images/banner.png",
"id": "9hEkA5JsvAdlSrocam",
"locked": false,
"note": "",
"pleroma": {
"confirmation_pending": false,
"hide_favorites": false,
"hide_followers": false,
"hide_follows": false,
"is_admin": false,
"is_moderator": false,
"relationship": {},
"tags": []
},
"source": {
"note": "",
"pleroma": {},
"sensitive": false
},
"statuses_count": 1,
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
"content": "Please delete it",
"created_at": "2019-04-29T19:48:15.000Z",
"id": "9iJGOv1j8hxuw19bcm",
"state": "open",
"statuses": [
{
"account": { ... },
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "<span class=\"h-card\"><a data-user=\"9hEkA5JsvAdlSrocam\" class=\"u-url mention\" href=\"https://pleroma.example.org/users/lain\">@<span>lain</span></a></span> click on my link <a href=\"https://www.google.com/\">https://www.google.com/</a>",
"created_at": "2019-04-23T19:15:47.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9i6mQ9uVrrOmOime8m",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "lain",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "@lain click on my link https://www.google.com/"
},
"conversation_id": 28,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/8717b90f-8e09-4b58-97b0-e3305472b396",
"url": "https://pleroma.example.org/notice/9i6mQ9uVrrOmOime8m",
"visibility": "direct"
}
]
}
]
}
```
## `/api/pleroma/admin/reports/:id`
### Get an individual report
- Method `GET`
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id`
### Change the state of the report
- Method `PUT`
- Params:
- `id`
- `state`: required, the new state. Valid values are `open`, `closed` and `resolved`
- Response:
- On failure:
- 400 Bad Request `"Unsupported state"`
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id/respond`
### Respond to a report
- Method `POST`
- Params:
- `id`
- `status`: required, the message
- Response:
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, created Mastodon Status entity
```json
{
"account": { ... },
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "Your claim is going to be closed",
"created_at": "2019-05-11T17:13:03.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9ihuiSL1405I65TmEq",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
{
"acct": "admin",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/admin",
"username": "admin"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "Your claim is going to be closed"
},
"conversation_id": 35,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/cab0836d-9814-46cd-a0ea-529da9db5fcb",
"url": "https://pleroma.example.org/notice/9ihuiSL1405I65TmEq",
"visibility": "direct"
}
```
## `/api/pleroma/admin/statuses/:id`
### Change the scope of an individual reported status
- Method `PUT`
- Params:
- `id`
- `sensitive`: optional, valid values are `true` or `false`
- `visibility`: optional, valid values are `public`, `private` and `unlisted`
- Response:
- On failure:
- 400 Bad Request `"Unsupported visibility"`
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Mastodon Status entity
## `/api/pleroma/admin/statuses/:id`
### Delete an individual reported status
- Method `DELETE`
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: 200 OK `{}`

View file

@ -87,3 +87,13 @@ Additional parameters can be added to the JSON body/Form data:
`POST /oauth/token` `POST /oauth/token`
Post here request with grant_type=refresh_token to obtain new access token. Returns an access token. Post here request with grant_type=refresh_token to obtain new access token. Returns an access token.
## Account Registration
`POST /api/v1/accounts`
Has theses additionnal parameters (which are the same as in Pleroma-API):
* `fullname`: optional
* `bio`: optional
* `captcha_solution`: optional, contains provider-specific captcha solution,
* `captcha_token`: optional, contains provider-specific captcha token
* `token`: invite token required when the registerations aren't public.

View file

@ -61,6 +61,15 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
* Response: JSON. Returns `{"status": "success"}` if the deletion was successful, `{"error": "[error message]"}` otherwise * Response: JSON. Returns `{"status": "success"}` if the deletion was successful, `{"error": "[error message]"}` otherwise
* Example response: `{"error": "Invalid password."}` * Example response: `{"error": "Invalid password."}`
## `/api/pleroma/disable_account`
### Disable an account
* Method `POST`
* Authentication: required
* Params:
* `password`: user's password
* Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise
* Example response: `{"error": "Invalid password."}`
## `/api/account/register` ## `/api/account/register`
### Register a new user ### Register a new user
* Method `POST` * Method `POST`

View file

@ -105,6 +105,12 @@ config :pleroma, Pleroma.Emails.Mailer,
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`) * `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`)
* `healthcheck`: if set to true, system data will be shown on ``/api/pleroma/healthcheck``. * `healthcheck`: if set to true, system data will be shown on ``/api/pleroma/healthcheck``.
## :app_account_creation
REST API for creating an account settings
* `enabled`: Enable/disable registration
* `max_requests`: Number of requests allowed for creating accounts
* `interval`: Interval for restricting requests for one ip (seconds)
## :logger ## :logger
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack

View file

@ -12,6 +12,7 @@ This guide will assume you are on Debian Stretch. This guide should also work wi
* `erlang-tools` * `erlang-tools`
* `erlang-parsetools` * `erlang-parsetools`
* `erlang-eldap`, if you want to enable ldap authenticator * `erlang-eldap`, if you want to enable ldap authenticator
* `erlang-ssh`
* `erlang-xmerl` * `erlang-xmerl`
* `git` * `git`
* `build-essential` * `build-essential`
@ -49,7 +50,7 @@ sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
```shell ```shell
sudo apt update sudo apt update
sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh
``` ```
### Install PleromaBE ### Install PleromaBE

View file

@ -14,6 +14,7 @@
- erlang-dev - erlang-dev
- erlang-tools - erlang-tools
- erlang-parsetools - erlang-parsetools
- erlang-ssh
- erlang-xmerl (Jessieではバックポートからインストールすること) - erlang-xmerl (Jessieではバックポートからインストールすること)
- git - git
- build-essential - build-essential
@ -44,7 +45,7 @@ wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
* ElixirとErlangをインストールします、 * ElixirとErlangをインストールします、
``` ```
apt update && apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools apt update && apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh
``` ```
### Pleroma BE (バックエンド) をインストールします ### Pleroma BE (バックエンド) をインストールします

View file

@ -4,6 +4,7 @@
defmodule Mix.Tasks.Pleroma.Database do defmodule Mix.Tasks.Pleroma.Database do
alias Mix.Tasks.Pleroma.Common alias Mix.Tasks.Pleroma.Common
alias Pleroma.Conversation
require Logger require Logger
use Mix.Task use Mix.Task
@ -19,6 +20,11 @@ defmodule Mix.Tasks.Pleroma.Database do
Options: Options:
- `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references - `--vacuum` - run `VACUUM FULL` after the embedded objects are replaced with their references
## Create a conversation for all existing DMs. Can be safely re-run.
mix pleroma.database bump_all_conversations
""" """
def run(["remove_embedded_objects" | args]) do def run(["remove_embedded_objects" | args]) do
{options, [], []} = {options, [], []} =
@ -48,4 +54,9 @@ def run(["remove_embedded_objects" | args]) do
) )
end end
end end
def run(["bump_all_conversations"]) do
Common.start_pleroma()
Conversation.bump_for_all_activities()
end
end end

View file

@ -137,7 +137,7 @@ def run(["get-packs" | args]) do
]) ])
) )
files = Tesla.get!(client(), files_url).body |> Poison.decode!() files = Tesla.get!(client(), files_url).body |> Jason.decode!()
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name])) IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
@ -239,7 +239,7 @@ def run(["gen-pack", src]) do
emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts) emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)
File.write!(files_name, Poison.encode!(emoji_map, pretty: true)) File.write!(files_name, Jason.encode!(emoji_map, pretty: true))
IO.puts(""" IO.puts("""
@ -248,11 +248,11 @@ def run(["gen-pack", src]) do
""") """)
if File.exists?("index.json") do if File.exists?("index.json") do
existing_data = File.read!("index.json") |> Poison.decode!() existing_data = File.read!("index.json") |> Jason.decode!()
File.write!( File.write!(
"index.json", "index.json",
Poison.encode!( Jason.encode!(
Map.merge( Map.merge(
existing_data, existing_data,
pack_json pack_json
@ -263,14 +263,14 @@ def run(["gen-pack", src]) do
IO.puts("index.json file has been update with the #{name} pack") IO.puts("index.json file has been update with the #{name} pack")
else else
File.write!("index.json", Poison.encode!(pack_json, pretty: true)) File.write!("index.json", Jason.encode!(pack_json, pretty: true))
IO.puts("index.json has been created with the #{name} pack") IO.puts("index.json has been created with the #{name} pack")
end end
end end
defp fetch_manifest(from) do defp fetch_manifest(from) do
Poison.decode!( Jason.decode!(
if String.starts_with?(from, "http") do if String.starts_with?(from, "http") do
Tesla.get!(client(), from).body Tesla.get!(client(), from).body
else else

View file

@ -77,6 +77,10 @@ defmodule Mix.Tasks.Pleroma.User do
## Delete tags from a user. ## Delete tags from a user.
mix pleroma.user untag NICKNAME TAGS mix pleroma.user untag NICKNAME TAGS
## Toggle confirmation of the user's account.
mix pleroma.user toggle_confirmed NICKNAME
""" """
def run(["new", nickname, email | rest]) do def run(["new", nickname, email | rest]) do
{options, [], []} = {options, [], []} =
@ -138,7 +142,7 @@ def run(["new", nickname, email | rest]) do
bio: bio bio: bio
} }
changeset = User.register_changeset(%User{}, params, confirmed: true) changeset = User.register_changeset(%User{}, params, need_confirmation: false)
{:ok, _user} = User.register(changeset) {:ok, _user} = User.register(changeset)
Mix.shell().info("User #{nickname} created") Mix.shell().info("User #{nickname} created")
@ -388,6 +392,21 @@ def run(["delete_activities", nickname]) do
end end
end end
def run(["toggle_confirmed", nickname]) do
Common.start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.toggle_confirmation(user)
message = if user.info.confirmation_pending, do: "needs", else: "doesn't need"
Mix.shell().info("#{nickname} #{message} confirmation.")
else
_ ->
Mix.shell().error("No local user #{nickname}")
end
end
defp set_moderator(user, value) do defp set_moderator(user, value) do
info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value}) info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value})

View file

@ -60,21 +60,24 @@ defmodule Pleroma.Activity do
timestamps() timestamps()
end end
def with_preloaded_object(query) do def with_joined_object(query) do
query join(query, :inner, [activity], o in Object,
|> join(
:inner,
[activity],
o in Object,
on: on:
fragment( fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')", "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.data, o.data,
activity.data, activity.data,
activity.data activity.data
) ),
as: :object
) )
|> preload([activity, object], object: object) end
def with_preloaded_object(query) do
query
|> has_named_binding?(:object)
|> if(do: query, else: with_joined_object(query))
|> preload([activity, object: object], object: object)
end end
def with_preloaded_bookmark(query, %User{} = user) do def with_preloaded_bookmark(query, %User{} = user) do
@ -108,7 +111,7 @@ def get_bookmark(_, _), do: nil
def change(struct, params \\ %{}) do def change(struct, params \\ %{}) do
struct struct
|> cast(params, [:data]) |> cast(params, [:data, :recipients])
|> validate_required([:data]) |> validate_required([:data])
|> unique_constraint(:ap_id, name: :activities_unique_apid_index) |> unique_constraint(:ap_id, name: :activities_unique_apid_index)
end end
@ -132,7 +135,10 @@ def get_by_ap_id_with_object(ap_id) do
end end
def get_by_id(id) do def get_by_id(id) do
Repo.get(Activity, id) Activity
|> where([a], a.id == ^id)
|> restrict_deactivated_users()
|> Repo.one()
end end
def get_by_id_with_object(id) do def get_by_id_with_object(id) do
@ -200,6 +206,7 @@ def get_all_create_by_object_ap_id(ap_id) do
def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
create_by_object_ap_id(ap_id) create_by_object_ap_id(ap_id)
|> restrict_deactivated_users()
|> Repo.one() |> Repo.one()
end end
@ -314,4 +321,14 @@ def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
def query_by_actor(actor) do def query_by_actor(actor) do
from(a in Activity, where: a.actor == ^actor) from(a in Activity, where: a.actor == ^actor)
end end
def restrict_deactivated_users(query) do
from(activity in query,
where:
fragment(
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
activity.actor
)
)
end
end end

View file

@ -95,7 +95,6 @@ def handle_command(state, "home") do
activities = activities =
[user.ap_id | user.following] [user.ap_id | user.following]
|> ActivityPub.fetch_activities(params) |> ActivityPub.fetch_activities(params)
|> ActivityPub.contain_timeline(user)
Enum.each(activities, fn activity -> Enum.each(activities, fn activity ->
puts_activity(activity) puts_activity(activity)

View file

@ -15,7 +15,7 @@ def new do
%{error: "Kocaptcha service unavailable"} %{error: "Kocaptcha service unavailable"}
{:ok, res} -> {:ok, res} ->
json_resp = Poison.decode!(res.body) json_resp = Jason.decode!(res.body)
%{ %{
type: :kocaptcha, type: :kocaptcha,

View file

@ -12,8 +12,12 @@ def get(key), do: get(key, nil)
def get([key], default), do: get(key, default) def get([key], default), do: get(key, default)
def get([parent_key | keys], default) do def get([parent_key | keys], default) do
Application.get_env(:pleroma, parent_key) case :pleroma
|> get_in(keys) || default |> Application.get_env(parent_key)
|> get_in(keys) do
nil -> default
any -> any
end
end end
def get(key, default) do def get(key, default) do

View file

@ -5,15 +5,6 @@
defmodule Pleroma.Config.DeprecationWarnings do defmodule Pleroma.Config.DeprecationWarnings do
require Logger require Logger
def check_frontend_config_mechanism do
if Pleroma.Config.get(:fe) do
Logger.warn("""
!!!DEPRECATION WARNING!!!
You are using the old configuration mechanism for the frontend. Please check config.md.
""")
end
end
def check_hellthread_threshold do def check_hellthread_threshold do
if Pleroma.Config.get([:mrf_hellthread, :threshold]) do if Pleroma.Config.get([:mrf_hellthread, :threshold]) do
Logger.warn(""" Logger.warn("""
@ -24,7 +15,6 @@ def check_hellthread_threshold do
end end
def warn do def warn do
check_frontend_config_mechanism()
check_hellthread_threshold() check_hellthread_threshold()
end end
end end

View file

@ -45,7 +45,7 @@ def get_for_ap_id(ap_id) do
2. Create a participation for all the people involved who don't have one already 2. Create a participation for all the people involved who don't have one already
3. Bump all relevant participations to 'unread' 3. Bump all relevant participations to 'unread'
""" """
def create_or_bump_for(activity) do def create_or_bump_for(activity, opts \\ []) do
with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity), with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
"Create" <- activity.data["type"], "Create" <- activity.data["type"],
object <- Pleroma.Object.normalize(activity), object <- Pleroma.Object.normalize(activity),
@ -58,7 +58,7 @@ def create_or_bump_for(activity) do
participations = participations =
Enum.map(users, fn user -> Enum.map(users, fn user ->
{:ok, participation} = {:ok, participation} =
Participation.create_for_user_and_conversation(user, conversation) Participation.create_for_user_and_conversation(user, conversation, opts)
participation participation
end) end)
@ -72,4 +72,21 @@ def create_or_bump_for(activity) do
e -> {:error, e} e -> {:error, e}
end end
end end
@doc """
This is only meant to be run by a mix task. It creates conversations/participations for all direct messages in the database.
"""
def bump_for_all_activities do
stream =
Pleroma.Web.ActivityPub.ActivityPub.fetch_direct_messages_query()
|> Repo.stream()
Repo.transaction(
fn ->
stream
|> Enum.each(fn a -> create_or_bump_for(a, read: true) end)
end,
timeout: :infinity
)
end
end end

View file

@ -22,15 +22,17 @@ defmodule Pleroma.Conversation.Participation do
def creation_cng(struct, params) do def creation_cng(struct, params) do
struct struct
|> cast(params, [:user_id, :conversation_id]) |> cast(params, [:user_id, :conversation_id, :read])
|> validate_required([:user_id, :conversation_id]) |> validate_required([:user_id, :conversation_id])
end end
def create_for_user_and_conversation(user, conversation) do def create_for_user_and_conversation(user, conversation, opts \\ []) do
read = !!opts[:read]
%__MODULE__{} %__MODULE__{}
|> creation_cng(%{user_id: user.id, conversation_id: conversation.id}) |> creation_cng(%{user_id: user.id, conversation_id: conversation.id, read: read})
|> Repo.insert( |> Repo.insert(
on_conflict: [set: [read: false, updated_at: NaiveDateTime.utc_now()]], on_conflict: [set: [read: read, updated_at: NaiveDateTime.utc_now()]],
returning: true, returning: true,
conflict_target: [:user_id, :conversation_id] conflict_target: [:user_id, :conversation_id]
) )

View file

@ -29,7 +29,7 @@ def report(to, reporter, account, statuses, comment) do
end end
statuses_html = statuses_html =
if length(statuses) > 0 do if is_list(statuses) && length(statuses) > 0 do
statuses_list_html = statuses_list_html =
statuses statuses
|> Enum.map(fn |> Enum.map(fn

View file

@ -38,7 +38,8 @@ def get_filters(%User{id: user_id} = _user) do
query = query =
from( from(
f in Pleroma.Filter, f in Pleroma.Filter,
where: f.user_id == ^user_id where: f.user_id == ^user_id,
order_by: [desc: :id]
) )
Repo.all(query) Repo.all(query)

View file

@ -77,13 +77,13 @@ def render_activities(activities) do
user = User.get_cached_by_ap_id(activity.data["actor"]) user = User.get_cached_by_ap_id(activity.data["actor"])
object = Object.normalize(activity) object = Object.normalize(activity)
like_count = object["like_count"] || 0 like_count = object.data["like_count"] || 0
announcement_count = object["announcement_count"] || 0 announcement_count = object.data["announcement_count"] || 0
link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <> link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <>
info("#{like_count} likes, #{announcement_count} repeats") <> info("#{like_count} likes, #{announcement_count} repeats") <>
"i\tfake\t(NULL)\t0\r\n" <> "i\tfake\t(NULL)\t0\r\n" <>
info(HTML.strip_tags(String.replace(object["content"], "<br>", "\r"))) info(HTML.strip_tags(String.replace(object.data["content"], "<br>", "\r")))
end) end)
|> Enum.join("i\tfake\t(NULL)\t0\r\n") |> Enum.join("i\tfake\t(NULL)\t0\r\n")
end end

View file

@ -33,6 +33,13 @@ def changeset(%Notification{} = notification, attrs) do
def for_user_query(user) do def for_user_query(user) do
Notification Notification
|> where(user_id: ^user.id) |> where(user_id: ^user.id)
|> where(
[n, a],
fragment(
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
a.actor
)
)
|> join(:inner, [n], activity in assoc(n, :activity)) |> join(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object, |> join(:left, [n, a], object in Object,
on: on:

View file

@ -0,0 +1,31 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug do
import Plug.Conn
alias Pleroma.Config
alias Pleroma.User
def init(options) do
options
end
def call(conn, _) do
public? = Config.get!([:instance, :public])
case {public?, conn} do
{true, _} ->
conn
{false, %{assigns: %{user: %User{}}}} ->
conn
{false, _} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{error: "This resource requires authentication."}))
|> halt
end
end
end

View file

@ -4,7 +4,6 @@
defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.HTTPSignatures
import Plug.Conn import Plug.Conn
require Logger require Logger

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Plugs.OAuthPlug do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
@realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
@ -22,18 +23,39 @@ def call(%{params: %{"access_token" => access_token}} = conn, _) do
|> assign(:token, token_record) |> assign(:token, token_record)
|> assign(:user, user) |> assign(:user, user)
else else
_ -> conn _ ->
# token found, but maybe only with app
with {:ok, app, token_record} <- fetch_app_and_token(access_token) do
conn
|> assign(:token, token_record)
|> assign(:app, app)
else
_ -> conn
end
end end
end end
def call(conn, _) do def call(conn, _) do
with {:ok, token_str} <- fetch_token_str(conn), case fetch_token_str(conn) do
{:ok, user, token_record} <- fetch_user_and_token(token_str) do {:ok, token} ->
conn with {:ok, user, token_record} <- fetch_user_and_token(token) do
|> assign(:token, token_record) conn
|> assign(:user, user) |> assign(:token, token_record)
else |> assign(:user, user)
_ -> conn else
_ ->
# token found, but maybe only with app
with {:ok, app, token_record} <- fetch_app_and_token(token) do
conn
|> assign(:token, token_record)
|> assign(:app, app)
else
_ -> conn
end
end
_ ->
conn
end end
end end
@ -54,6 +76,16 @@ defp fetch_user_and_token(token) do
end end
end end
@spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil
defp fetch_app_and_token(token) do
query =
from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app])
with %Token{app: app} = token_record <- Repo.one(query) do
{:ok, app, token_record}
end
end
# Gets token from session by :oauth_token key # Gets token from session by :oauth_token key
# #
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}

View file

@ -0,0 +1,36 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.RateLimitPlug do
import Phoenix.Controller, only: [json: 2]
import Plug.Conn
def init(opts), do: opts
def call(conn, opts) do
enabled? = Pleroma.Config.get([:app_account_creation, :enabled])
case check_rate(conn, Map.put(opts, :enabled, enabled?)) do
{:ok, _count} -> conn
{:error, _count} -> render_error(conn)
%Plug.Conn{} = conn -> conn
end
end
defp check_rate(conn, %{enabled: true} = opts) do
max_requests = opts[:max_requests]
bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".")
ExRated.check_rate(bucket_name, opts[:interval] * 1000, max_requests)
end
defp check_rate(conn, _), do: conn
defp render_error(conn) do
conn
|> put_status(:forbidden)
|> json(%{error: "Rate limit exceeded."})
|> halt()
end
end

41
lib/pleroma/signature.ex Normal file
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.Signature do
@behaviour HTTPSignatures.Adapter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
def fetch_public_key(conn) do
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def refetch_public_key(conn) do
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def sign(%User{} = user, headers) do
with {:ok, %{info: %{keys: keys}}} <- WebFinger.ensure_keys_present(user),
{:ok, private_key, _} <- Salmon.keys_from_pem(keys) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
end
end
end

View file

@ -14,7 +14,7 @@ def process_url(url) do
def process_response_body(body) do def process_response_body(body) do
body body
|> Poison.decode!() |> Jason.decode!()
end end
def get_token do def get_token do
@ -38,7 +38,7 @@ def get_token do
end end
def make_auth_body(username, password, tenant) do def make_auth_body(username, password, tenant) do
Poison.encode!(%{ Jason.encode!(%{
:auth => %{ :auth => %{
:passwordCredentials => %{ :passwordCredentials => %{
:username => username, :username => username,

View file

@ -105,10 +105,8 @@ def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
def user_info(%User{} = user) do def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0
%{ %{
following_count: length(user.following) - oneself, following_count: following_count(user),
note_count: user.info.note_count, note_count: user.info.note_count,
follower_count: user.info.follower_count, follower_count: user.info.follower_count,
locked: user.info.locked, locked: user.info.locked,
@ -117,6 +115,20 @@ def user_info(%User{} = user) do
} }
end end
def restrict_deactivated(query) do
from(u in query,
where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
)
end
def following_count(%User{following: []}), do: 0
def following_count(%User{} = user) do
user
|> get_friends_query()
|> Repo.aggregate(:count, :id)
end
def remote_user_creation(params) do def remote_user_creation(params) do
params = params =
params params
@ -204,14 +216,15 @@ def reset_password(user, data) do
end end
def register_changeset(struct, params \\ %{}, opts \\ []) do def register_changeset(struct, params \\ %{}, opts \\ []) do
confirmation_status = need_confirmation? =
if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do if is_nil(opts[:need_confirmation]) do
:confirmed Pleroma.Config.get([:instance, :account_activation_required])
else else
:unconfirmed opts[:need_confirmation]
end end
info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status) info_change =
User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
changeset = changeset =
struct struct
@ -254,7 +267,7 @@ defp autofollow_users(user) do
candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
autofollowed_users = autofollowed_users =
User.Query.build(%{nickname: candidates, local: true}) User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|> Repo.all() |> Repo.all()
follow_all(user, autofollowed_users) follow_all(user, autofollowed_users)
@ -412,24 +425,6 @@ def following?(%User{} = follower, %User{} = followed) do
Enum.member?(follower.following, followed.follower_address) Enum.member?(follower.following, followed.follower_address)
end end
def follow_import(%User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
Enum.map(
followed_identifiers,
fn followed_identifier ->
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do
followed
else
err ->
Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
err
end
end
)
end
def locked?(%User{} = user) do def locked?(%User{} = user) do
user.info.locked || false user.info.locked || false
end end
@ -551,8 +546,7 @@ def get_or_fetch_by_nickname(nickname) do
with [_nick, _domain] <- String.split(nickname, "@"), with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do {:ok, user} <- fetch_by_nickname(nickname) do
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
# TODO turn into job fetch_initial_posts(user)
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end end
{:ok, user} {:ok, user}
@ -563,19 +557,12 @@ def get_or_fetch_by_nickname(nickname) do
end end
@doc "Fetch some posts when the user has just been federated with" @doc "Fetch some posts when the user has just been federated with"
def fetch_initial_posts(user) do def fetch_initial_posts(user),
pages = Pleroma.Config.get!([:fetch_initial_posts, :pages]) do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
Enum.each(
# Insert all the posts in reverse order, so they're in the right order on the timeline
Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
&Pleroma.Web.Federator.incoming_ap_doc/1
)
end
@spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_followers_query(%User{} = user, nil) do def get_followers_query(%User{} = user, nil) do
User.Query.build(%{followers: user}) User.Query.build(%{followers: user, deactivated: false})
end end
def get_followers_query(user, page) do def get_followers_query(user, page) do
@ -600,7 +587,7 @@ def get_followers_ids(user, page \\ nil) do
@spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_friends_query(%User{} = user, nil) do def get_friends_query(%User{} = user, nil) do
User.Query.build(%{friends: user}) User.Query.build(%{friends: user, deactivated: false})
end end
def get_friends_query(user, page) do def get_friends_query(user, page) do
@ -690,16 +677,16 @@ def update_note_count(%User{} = user) do
info_cng = User.Info.set_note_count(user.info, note_count) info_cng = User.Info.set_note_count(user.info, note_count)
cng = user
change(user) |> change()
|> put_embed(:info, info_cng) |> put_embed(:info, info_cng)
|> update_and_set_cache()
update_and_set_cache(cng)
end end
def update_follower_count(%User{} = user) do def update_follower_count(%User{} = user) do
follower_count_query = follower_count_query =
User.Query.build(%{followers: user}) |> select([u], %{count: count(u.id)}) User.Query.build(%{followers: user, deactivated: false})
|> select([u], %{count: count(u.id)})
User User
|> where(id: ^user.id) |> where(id: ^user.id)
@ -724,7 +711,7 @@ def update_follower_count(%User{} = user) do
@spec get_users_from_set([String.t()], boolean()) :: [User.t()] @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
def get_users_from_set(ap_ids, local_only \\ true) do def get_users_from_set(ap_ids, local_only \\ true) do
criteria = %{ap_id: ap_ids} criteria = %{ap_id: ap_ids, deactivated: false}
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
User.Query.build(criteria) User.Query.build(criteria)
@ -733,7 +720,7 @@ def get_users_from_set(ap_ids, local_only \\ true) do
@spec get_recipients_from_activity(Activity.t()) :: [User.t()] @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
def get_recipients_from_activity(%Activity{recipients: to}) do def get_recipients_from_activity(%Activity{recipients: to}) do
User.Query.build(%{recipients_from_activity: to, local: true}) User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
|> Repo.all() |> Repo.all()
end end
@ -831,6 +818,7 @@ defp fts_search_subquery(term, query \\ User) do
^processed_query ^processed_query
) )
) )
|> restrict_deactivated()
end end
defp trigram_search_subquery(term) do defp trigram_search_subquery(term) do
@ -849,23 +837,7 @@ defp trigram_search_subquery(term) do
}, },
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term) where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
) )
end |> restrict_deactivated()
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
Enum.map(
blocked_identifiers,
fn blocked_identifier ->
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, blocker} <- block(blocker, blocked),
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked
else
err ->
Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
err
end
end
)
end end
def mute(muter, %User{ap_id: ap_id}) do def mute(muter, %User{ap_id: ap_id}) do
@ -998,19 +970,19 @@ def subscribed_to?(user, %{ap_id: ap_id}) do
@spec muted_users(User.t()) :: [User.t()] @spec muted_users(User.t()) :: [User.t()]
def muted_users(user) do def muted_users(user) do
User.Query.build(%{ap_id: user.info.mutes}) User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
|> Repo.all() |> Repo.all()
end end
@spec blocked_users(User.t()) :: [User.t()] @spec blocked_users(User.t()) :: [User.t()]
def blocked_users(user) do def blocked_users(user) do
User.Query.build(%{ap_id: user.info.blocks}) User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
|> Repo.all() |> Repo.all()
end end
@spec subscribers(User.t()) :: [User.t()] @spec subscribers(User.t()) :: [User.t()]
def subscribers(user) do def subscribers(user) do
User.Query.build(%{ap_id: user.info.subscribers}) User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
|> Repo.all() |> Repo.all()
end end
@ -1038,14 +1010,25 @@ def unblock_domain(user, domain) do
update_and_set_cache(cng) update_and_set_cache(cng)
end end
def deactivate_async(user, status \\ true) do
PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
end
def deactivate(%User{} = user, status \\ true) do def deactivate(%User{} = user, status \\ true) do
info_cng = User.Info.set_activation_status(user.info, status) info_cng = User.Info.set_activation_status(user.info, status)
cng = with {:ok, friends} <- User.get_friends(user),
change(user) {:ok, followers} <- User.get_followers(user),
|> put_embed(:info, info_cng) {:ok, user} <-
user
|> change()
|> put_embed(:info, info_cng)
|> update_and_set_cache() do
Enum.each(followers, &invalidate_cache(&1))
Enum.each(friends, &update_follower_count(&1))
update_and_set_cache(cng) {:ok, user}
end
end end
def update_notification_settings(%User{} = user, settings \\ %{}) do def update_notification_settings(%User{} = user, settings \\ %{}) do
@ -1076,6 +1059,75 @@ def perform(:delete, %User{} = user) do
delete_user_activities(user) delete_user_activities(user)
end end
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:fetch_initial_posts, %User{} = user) do
pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
Enum.each(
# Insert all the posts in reverse order, so they're in the right order on the timeline
Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
&Pleroma.Web.Federator.incoming_ap_doc/1
)
{:ok, user}
end
def perform(:deactivate_async, user, status), do: deactivate(user, status)
@spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
when is_list(blocked_identifiers) do
Enum.map(
blocked_identifiers,
fn blocked_identifier ->
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, blocker} <- block(blocker, blocked),
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked
else
err ->
Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
err
end
end
)
end
@spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
def perform(:follow_import, %User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
Enum.map(
followed_identifiers,
fn followed_identifier ->
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do
followed
else
err ->
Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
err
end
end
)
end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
do:
PleromaJobQueue.enqueue(:background, __MODULE__, [
:blocks_import,
blocker,
blocked_identifiers
])
def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
do:
PleromaJobQueue.enqueue(:background, __MODULE__, [
:follow_import,
follower,
followed_identifiers
])
def delete_user_activities(%User{ap_id: ap_id} = user) do def delete_user_activities(%User{ap_id: ap_id} = user) do
stream = stream =
ap_id ap_id
@ -1129,8 +1181,8 @@ def get_or_fetch_by_ap_id(ap_id) do
resp = fetch_by_ap_id(ap_id) resp = fetch_by_ap_id(ap_id)
if should_fetch_initial do if should_fetch_initial do
with {:ok, %User{} = user} = resp do with {:ok, %User{} = user} <- resp do
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) fetch_initial_posts(user)
end end
end end
@ -1319,11 +1371,24 @@ def error_user(ap_id) do
@spec all_superusers() :: [User.t()] @spec all_superusers() :: [User.t()]
def all_superusers do def all_superusers do
User.Query.build(%{super_users: true, local: true}) User.Query.build(%{super_users: true, local: true, deactivated: false})
|> Repo.all() |> Repo.all()
end end
def showing_reblogs?(%User{} = user, %User{} = target) do def showing_reblogs?(%User{} = user, %User{} = target) do
target.ap_id not in user.info.muted_reblogs target.ap_id not in user.info.muted_reblogs
end end
@spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
def toggle_confirmation(%User{} = user) do
need_confirmation? = !user.info.confirmation_pending
info_changeset =
User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
user
|> change()
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
end
end end

View file

@ -8,6 +8,8 @@ defmodule Pleroma.User.Info do
alias Pleroma.User.Info alias Pleroma.User.Info
@type t :: %__MODULE__{}
embedded_schema do embedded_schema do
field(:banner, :map, default: %{}) field(:banner, :map, default: %{})
field(:background, :map, default: %{}) field(:background, :map, default: %{})
@ -210,21 +212,23 @@ def profile_update(info, params) do
]) ])
end end
def confirmation_changeset(info, :confirmed) do @spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t()
confirmation_changeset(info, %{ def confirmation_changeset(info, opts) do
confirmation_pending: false, need_confirmation? = Keyword.get(opts, :need_confirmation)
confirmation_token: nil
})
end
def confirmation_changeset(info, :unconfirmed) do params =
confirmation_changeset(info, %{ if need_confirmation? do
confirmation_pending: true, %{
confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64() confirmation_pending: true,
}) confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
end }
else
%{
confirmation_pending: false,
confirmation_token: nil
}
end
def confirmation_changeset(info, params) do
cast(info, params, [:confirmation_pending, :confirmation_token]) cast(info, params, [:confirmation_pending, :confirmation_token])
end end

View file

@ -118,7 +118,11 @@ defp compose_query({:active, _}, query) do
|> where([u], not is_nil(u.nickname)) |> where([u], not is_nil(u.nickname))
end end
defp compose_query({:deactivated, _}, query) do defp compose_query({:deactivated, false}, query) do
User.restrict_deactivated(query)
end
defp compose_query({:deactivated, true}, query) do
where(query, [u], fragment("?->'deactivated' @> 'true'", u.info)) where(query, [u], fragment("?->'deactivated' @> 'true'", u.info))
|> where([u], not is_nil(u.nickname)) |> where([u], not is_nil(u.nickname))
end end

View file

@ -5,7 +5,6 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Instances
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Fetcher alias Pleroma.Object.Fetcher
@ -15,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
import Ecto.Query import Ecto.Query
@ -24,8 +22,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
require Logger require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
# For Announce activities, we filter the recipients based on following status for any actors # For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary. # that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do defp get_recipients(%{"type" => "Announce"} = data) do
@ -137,9 +133,7 @@ def insert(map, local \\ true, fake \\ false) when is_map(map) do
activity activity
end end
Task.start(fn -> PleromaJobQueue.enqueue(:background, Pleroma.Web.RichMedia.Helpers, [:fetch, activity])
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end)
Notification.create_notifications(activity) Notification.create_notifications(activity)
@ -546,8 +540,6 @@ defp restrict_visibility(query, %{visibility: visibility})
) )
) )
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
query query
else else
Logger.error("Could not restrict visibility to #{visibility}") Logger.error("Could not restrict visibility to #{visibility}")
@ -563,8 +555,6 @@ defp restrict_visibility(query, %{visibility: visibility})
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility) fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
) )
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
query query
end end
@ -575,6 +565,18 @@ defp restrict_visibility(_query, %{visibility: visibility})
defp restrict_visibility(query, _visibility), do: query defp restrict_visibility(query, _visibility), do: query
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}) do
query =
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
)
query
end
defp restrict_thread_visibility(query, _), do: query
def fetch_user_activities(user, reading_user, params \\ %{}) do def fetch_user_activities(user, reading_user, params \\ %{}) do
params = params =
params params
@ -701,6 +703,12 @@ defp restrict_type(query, %{"type" => type}) do
defp restrict_type(query, _), do: query defp restrict_type(query, _), do: query
defp restrict_state(query, %{"state" => state}) do
from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state))
end
defp restrict_state(query, _), do: query
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
from( from(
activity in query, activity in query,
@ -756,8 +764,11 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info.blocks || [] blocks = info.blocks || []
domain_blocks = info.domain_blocks || [] domain_blocks = info.domain_blocks || []
query =
if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
from( from(
activity in query, [activity, object: o] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks), where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (? && ?)", activity.recipients, ^blocks), where: fragment("not (? && ?)", activity.recipients, ^blocks),
where: where:
@ -767,7 +778,8 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
activity.data, activity.data,
^blocks ^blocks
), ),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks) where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks),
where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks)
) )
end end
@ -849,15 +861,18 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_local(opts) |> restrict_local(opts)
|> restrict_actor(opts) |> restrict_actor(opts)
|> restrict_type(opts) |> restrict_type(opts)
|> restrict_state(opts)
|> restrict_favorited_by(opts) |> restrict_favorited_by(opts)
|> restrict_blocked(opts) |> restrict_blocked(opts)
|> restrict_muted(opts) |> restrict_muted(opts)
|> restrict_media(opts) |> restrict_media(opts)
|> restrict_visibility(opts) |> restrict_visibility(opts)
|> restrict_thread_visibility(opts)
|> restrict_replies(opts) |> restrict_replies(opts)
|> restrict_reblogs(opts) |> restrict_reblogs(opts)
|> restrict_pinned(opts) |> restrict_pinned(opts)
|> restrict_muted_reblogs(opts) |> restrict_muted_reblogs(opts)
|> Activity.restrict_deactivated_users()
end end
def fetch_activities(recipients, opts \\ %{}) do def fetch_activities(recipients, opts \\ %{}) do
@ -961,89 +976,6 @@ def make_user_from_nickname(nickname) do
end end
end end
def should_federate?(inbox, public) do
if public do
true
else
inbox_info = URI.parse(inbox)
!Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
end
def publish(actor, activity) do
remote_followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
public = is_public?(activity)
{: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}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
Federator.publish_single_ap(%{
inbox: inbox,
json: json,
actor: actor,
id: activity.data["id"],
unreachable_since: unreachable_since
})
end)
end
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
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,
date: date
})
with {:ok, %{status: code}} when code in 200..299 <-
result =
@httpoison.post(
inbox,
json,
[
{"Content-Type", "application/activity+json"},
{"Date", date},
{"signature", signature},
{"digest", digest}
]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(inbox)
result
else
{_post_result, response} ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
{:error, response}
end
end
# filter out broken threads # filter out broken threads
def contain_broken_threads(%Activity{} = activity, %User{} = user) do def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user) entire_thread_visible_for_user?(activity, user)
@ -1054,11 +986,10 @@ def contain_activity(%Activity{} = activity, %User{} = user) do
contain_broken_threads(activity, user) contain_broken_threads(activity, user)
end end
# do post-processing on a timeline def fetch_direct_messages_query do
def contain_timeline(timeline, user) do Activity
timeline |> restrict_type(%{"type" => "Create"})
|> Enum.filter(fn activity -> |> restrict_visibility(%{visibility: "direct"})
contain_activity(activity, user) |> order_by([activity], asc: activity.id)
end)
end end
end end

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.Web.ActivityPub.Publisher do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Instances
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Web.ActivityPub.Visibility
@behaviour Pleroma.Web.Federator.Publisher
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
@moduledoc """
ActivityPub outgoing federation module.
"""
@doc """
Determine if an activity can be represented by running it through Transmogrifier.
"""
def is_representable?(%Activity{} = activity) do
with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
true
else
_e ->
false
end
end
@doc """
Publish a single message to a peer. Takes a struct with the following
parameters set:
* `inbox`: the inbox to publish to
* `json`: the JSON message body representing the ActivityPub message
* `actor`: the actor which is signing the message
* `id`: the ActivityStreams URI of the message
"""
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
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.Signature.sign(actor, %{
host: host,
"content-length": byte_size(json),
digest: digest,
date: date
})
with {:ok, %{status: code}} when code in 200..299 <-
result =
@httpoison.post(
inbox,
json,
[
{"Content-Type", "application/activity+json"},
{"Date", date},
{"signature", signature},
{"digest", digest}
]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(inbox)
result
else
{_post_result, response} ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
{:error, response}
end
end
defp should_federate?(inbox, public) do
if public do
true
else
inbox_info = URI.parse(inbox)
!Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
end
@doc """
Publishes an activity to all relevant peers.
"""
def publish(%User{} = actor, %Activity{} = activity) do
remote_followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
public = is_public?(activity)
if public && Config.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
end
{: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}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
Pleroma.Web.Federator.Publisher.enqueue_one(
__MODULE__,
%{
inbox: inbox,
json: json,
actor: actor,
id: activity.data["id"],
unreachable_since: unreachable_since
}
)
end)
end
def gather_webfinger_links(%User{} = user) do
[
%{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
%{
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href" => user.ap_id
}
]
end
def gather_nodeinfo_protocol_names, do: ["activitypub"]
end

View file

@ -20,6 +20,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
require Logger require Logger
@supported_object_types ["Article", "Note", "Video", "Page"] @supported_object_types ["Article", "Note", "Video", "Page"]
@supported_report_states ~w(open closed resolved)
@valid_visibilities ~w(public unlisted private direct)
# Some implementations send the actor URI as the actor field, others send the entire actor object, # Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have. # so figure out what the actor's URI is based on what we have.
@ -670,7 +672,8 @@ def make_flag_data(params, additional) do
"actor" => params.actor.ap_id, "actor" => params.actor.ap_id,
"content" => params.content, "content" => params.content,
"object" => object, "object" => object,
"context" => params.context "context" => params.context,
"state" => "open"
} }
|> Map.merge(additional) |> Map.merge(additional)
end end
@ -682,7 +685,7 @@ def make_flag_data(params, additional) do
""" """
def fetch_ordered_collection(from, pages_left, acc \\ []) do def fetch_ordered_collection(from, pages_left, acc \\ []) do
with {:ok, response} <- Tesla.get(from), with {:ok, response} <- Tesla.get(from),
{:ok, collection} <- Poison.decode(response.body) do {:ok, collection} <- Jason.decode(response.body) do
case collection["type"] do case collection["type"] do
"OrderedCollection" -> "OrderedCollection" ->
# If we've encountered the OrderedCollection and not the page, # If we've encountered the OrderedCollection and not the page,
@ -713,4 +716,77 @@ def fetch_ordered_collection(from, pages_left, acc \\ []) do
end end
end end
end end
#### Report-related helpers
def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
with new_data <- Map.put(activity.data, "state", state),
changeset <- Changeset.change(activity, data: new_data),
{:ok, activity} <- Repo.update(changeset) do
{:ok, activity}
end
end
def update_report_state(_, _), do: {:error, "Unsupported state"}
def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
[to, cc, recipients] =
activity
|> get_updated_targets(visibility)
|> Enum.map(&Enum.uniq/1)
object_data =
activity.object.data
|> Map.put("to", to)
|> Map.put("cc", cc)
{:ok, object} =
activity.object
|> Object.change(%{data: object_data})
|> Object.update_and_set_cache()
activity_data =
activity.data
|> Map.put("to", to)
|> Map.put("cc", cc)
activity
|> Map.put(:object, object)
|> Activity.change(%{data: activity_data, recipients: recipients})
|> Repo.update()
end
def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
defp get_updated_targets(
%Activity{data: %{"to" => to} = data, recipients: recipients},
visibility
) do
cc = Map.get(data, "cc", [])
follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
public = "https://www.w3.org/ns/activitystreams#Public"
case visibility do
"public" ->
to = [public | List.delete(to, follower_address)]
cc = [follower_address | List.delete(cc, public)]
recipients = [public | recipients]
[to, cc, recipients]
"private" ->
to = [follower_address | List.delete(to, public)]
cc = List.delete(cc, public)
recipients = List.delete(recipients, public)
[to, cc, recipients]
"unlisted" ->
to = [follower_address | List.delete(to, public)]
cc = [public | List.delete(cc, follower_address)]
recipients = recipients ++ [follower_address, public]
[to, cc, recipients]
_ ->
[to, cc, recipients]
end
end
end end

View file

@ -1,6 +1,7 @@
defmodule Pleroma.Web.ActivityPub.Visibility do defmodule Pleroma.Web.ActivityPub.Visibility do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
@ -13,11 +14,12 @@ def is_public?(data) do
end end
def is_private?(activity) do def is_private?(activity) do
unless is_public?(activity) do with false <- is_public?(activity),
follower_address = User.get_cached_by_ap_id(activity.data["actor"]).follower_address %User{follower_address: follower_address} <-
Enum.any?(activity.data["to"], &(&1 == follower_address)) User.get_cached_by_ap_id(activity.data["actor"]) do
follower_address in activity.data["to"]
else else
false _ -> false
end end
end end
@ -38,24 +40,37 @@ def visible_for_user?(activity, user) do
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y)) visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end end
# guard def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
def entire_thread_visible_for_user?(nil, _user), do: false {:ok, %{rows: [[result]]}} =
Ecto.Adapters.SQL.query(Repo, "SELECT thread_visibility($1, $2)", [
user.ap_id,
activity.data["id"]
])
# XXX: Probably even more inefficient than the previous implementation intended to be a placeholder untill https://git.pleroma.social/pleroma/pleroma/merge_requests/971 is in develop result
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength end
def entire_thread_visible_for_user?( def get_visibility(object) do
%Activity{} = tail, public = "https://www.w3.org/ns/activitystreams#Public"
# %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail, to = object.data["to"] || []
user cc = object.data["cc"] || []
) do
case Object.normalize(tail) do
%{data: %{"inReplyTo" => parent_id}} when is_binary(parent_id) ->
parent = Activity.get_in_reply_to_activity(tail)
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
_ -> cond do
visible_for_user?(tail, user) public in to ->
"public"
public in cc ->
"unlisted"
# this should use the sql for the object's activity
Enum.any?(to, &String.contains?(&1, "/followers")) ->
"private"
length(cc) > 0 ->
"private"
true ->
"direct"
end end
end end
end end

View file

@ -4,11 +4,16 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIController do defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@ -59,7 +64,7 @@ def user_create(
bio: "." bio: "."
} }
changeset = User.register_changeset(%User{}, user_data, confirmed: true) changeset = User.register_changeset(%User{}, user_data, need_confirmation: false)
{:ok, user} = User.register(changeset) {:ok, user} = User.register(changeset)
conn conn
@ -287,12 +292,88 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
|> json(token.token) |> json(token.token)
end end
def list_reports(conn, params) do
params =
params
|> Map.put("type", "Flag")
|> Map.put("skip_preload", true)
reports =
[]
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
conn
|> put_view(ReportView)
|> render("index.json", %{reports: reports})
end
def report_show(conn, %{"id" => id}) do
with %Activity{} = report <- Activity.get_by_id(id) do
conn
|> put_view(ReportView)
|> render("show.json", %{report: report})
else
_ -> {:error, :not_found}
end
end
def report_update_state(conn, %{"id" => id, "state" => state}) do
with {:ok, report} <- CommonAPI.update_report_state(id, state) do
conn
|> put_view(ReportView)
|> render("show.json", %{report: report})
end
end
def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
with false <- is_nil(params["status"]),
%Activity{} <- Activity.get_by_id(id) do
params =
params
|> Map.put("in_reply_to_status_id", id)
|> Map.put("visibility", "direct")
{:ok, activity} = CommonAPI.post(user, params)
conn
|> put_view(StatusView)
|> render("status.json", %{activity: activity})
else
true ->
{:param_cast, nil}
nil ->
{:error, :not_found}
end
end
def status_update(conn, %{"id" => id} = params) do
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
conn
|> put_view(StatusView)
|> render("status.json", %{activity: activity})
end
end
def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
end
end
def errors(conn, {:error, :not_found}) do def errors(conn, {:error, :not_found}) do
conn conn
|> put_status(404) |> put_status(404)
|> json("Not found") |> json("Not found")
end end
def errors(conn, {:error, reason}) do
conn
|> put_status(400)
|> json(reason)
end
def errors(conn, {:param_cast, _}) do def errors(conn, {:param_cast, _}) do
conn conn
|> put_status(400) |> put_status(400)

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.Web.AdminAPI.ReportView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{reports: reports}) do
%{
reports: render_many(reports, __MODULE__, "show.json", as: :report)
}
end
def render("show.json", %{report: report}) do
user = User.get_cached_by_ap_id(report.data["actor"])
created_at = Utils.to_masto_date(report.data["published"])
[account_ap_id | status_ap_ids] = report.data["object"]
account = User.get_cached_by_ap_id(account_ap_id)
statuses =
Enum.map(status_ap_ids, fn ap_id ->
Activity.get_by_ap_id_with_object(ap_id)
end)
%{
id: report.id,
account: AccountView.render("account.json", %{user: account}),
actor: AccountView.render("account.json", %{user: user}),
content: report.data["content"],
created_at: created_at,
statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),
state: report.data["state"]
}
end
end

View file

@ -74,7 +74,7 @@ def create_from_registration(
password_confirmation: random_password password_confirmation: random_password
}, },
external: true, external: true,
confirmed: true need_confirmation: false
) )
|> Repo.insert(), |> Repo.insert(),
{:ok, _} <- {:ok, _} <-

View file

@ -71,6 +71,9 @@ def delete(activity_id, user) do
{:ok, _} <- unpin(activity_id, user), {:ok, _} <- unpin(activity_id, user),
{:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete} {:ok, delete}
else
_ ->
{:error, "Could not delete"}
end end
end end
@ -116,32 +119,34 @@ def unfavorite(id_or_ap_id, user) do
end end
end end
def get_visibility(%{"visibility" => visibility}) def get_visibility(%{"visibility" => visibility}, in_reply_to)
when visibility in ~w{public unlisted private direct}, when visibility in ~w{public unlisted private direct},
do: visibility do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{"in_reply_to_status_id" => status_id}) when not is_nil(status_id) do def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do
case get_replied_to_activity(status_id) do visibility = get_replied_to_visibility(in_reply_to)
nil -> {visibility, visibility}
"public" end
in_reply_to -> def get_visibility(_, in_reply_to), do: {"public", get_replied_to_visibility(in_reply_to)}
# XXX: these heuristics should be moved out of MastodonAPI.
with %Object{} = object <- Object.normalize(in_reply_to) do def get_replied_to_visibility(nil), do: nil
Pleroma.Web.MastodonAPI.StatusView.get_visibility(object)
end def get_replied_to_visibility(activity) do
with %Object{} = object <- Object.normalize(activity) do
Pleroma.Web.ActivityPub.Visibility.get_visibility(object)
end end
end end
def get_visibility(_), do: "public"
def post(user, %{"status" => status} = data) do def post(user, %{"status" => status} = data) do
visibility = get_visibility(data)
limit = Pleroma.Config.get([:instance, :limit]) limit = Pleroma.Config.get([:instance, :limit])
with status <- String.trim(status), with status <- String.trim(status),
attachments <- attachments_from_ids(data), attachments <- attachments_from_ids(data),
in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]), in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
{visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to),
{_, false} <-
{:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
{content_html, mentions, tags} <- {content_html, mentions, tags} <-
make_content_html( make_content_html(
status, status,
@ -185,6 +190,8 @@ def post(user, %{"status" => status} = data) do
) )
res res
else
e -> {:error, e}
end end
end end
@ -311,6 +318,60 @@ def report(user, data) do
end end
end end
def update_report_state(activity_id, state) do
with %Activity{} = activity <- Activity.get_by_id(activity_id),
{:ok, activity} <- Utils.update_report_state(activity, state) do
{:ok, activity}
else
nil ->
{:error, :not_found}
{:error, reason} ->
{:error, reason}
_ ->
{:error, "Could not update state"}
end
end
def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
{:ok, activity} <- toggle_sensitive(activity, opts),
{:ok, activity} <- set_visibility(activity, opts) do
{:ok, activity}
else
nil ->
{:error, :not_found}
{:error, reason} ->
{:error, reason}
end
end
defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
end
defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
when is_boolean(sensitive) do
new_data = Map.put(object.data, "sensitive", sensitive)
{:ok, object} =
object
|> Object.change(%{data: new_data})
|> Object.update_and_set_cache()
{:ok, Map.put(activity, :object, object)}
end
defp toggle_sensitive(activity, _), do: {:ok, activity}
defp set_visibility(activity, %{"visibility" => visibility}) do
Utils.update_activity_visibility(activity, visibility)
end
defp set_visibility(activity, _), do: {:ok, activity}
def hide_reblogs(user, muted) do def hide_reblogs(user, muted) do
ap_id = muted.ap_id ap_id = muted.ap_id

View file

@ -237,13 +237,11 @@ def make_note_data(
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq() "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
} }
if in_reply_to do with false <- is_nil(in_reply_to),
in_reply_to_object = Object.normalize(in_reply_to) %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
object
|> Map.put("inReplyTo", in_reply_to_object.data["id"])
else else
object _ -> object
end end
end end

View file

@ -7,13 +7,10 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Federator.RetryQueue alias Pleroma.Web.Federator.RetryQueue
alias Pleroma.Web.OStatus
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub alias Pleroma.Web.Websub
@ -42,14 +39,6 @@ def publish(activity, priority \\ 1) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority) PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)
end end
def publish_single_ap(params) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params])
end
def publish_single_websub(websub) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub])
end
def verify_websub(websub) do def verify_websub(websub) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub]) PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])
end end
@ -62,10 +51,6 @@ def refresh_subscriptions do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions]) PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
end end
def publish_single_salmon(params) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])
end
# Job Worker Callbacks # Job Worker Callbacks
def perform(:refresh_subscriptions) do def perform(:refresh_subscriptions) do
@ -95,23 +80,7 @@ def perform(:publish, activity) do
with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
{:ok, actor} = WebFinger.ensure_keys_present(actor) {:ok, actor} = WebFinger.ensure_keys_present(actor)
if Visibility.is_public?(activity) do Publisher.publish(actor, activity)
if OStatus.is_representable?(activity) do
Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end)
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end)
Pleroma.Web.Salmon.publish(actor, activity)
end
if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
end
end
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
Pleroma.Web.ActivityPub.ActivityPub.publish(actor, activity)
end end
end end
@ -148,25 +117,11 @@ def perform(:incoming_ap_doc, params) do
_e -> _e ->
# Just drop those for now # Just drop those for now
Logger.info("Unhandled activity") Logger.info("Unhandled activity")
Logger.info(Poison.encode!(params, pretty: 2)) Logger.info(Jason.encode!(params, pretty: true))
:error :error
end end
end end
def perform(:publish_single_salmon, params) do
Salmon.send_to_user(params)
end
def perform(:publish_single_ap, params) do
case ActivityPub.publish_one(params) do
{:ok, _} ->
:ok
{:error, _} ->
RetryQueue.enqueue(params, ActivityPub)
end
end
def perform( def perform(
:publish_single_websub, :publish_single_websub,
%{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params %{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params

View file

@ -0,0 +1,95 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator.Publisher do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.Federator.RetryQueue
require Logger
@moduledoc """
Defines the contract used by federation implementations to publish messages to
their peers.
"""
@doc """
Determine whether an activity can be relayed using the federation module.
"""
@callback is_representable?(Pleroma.Activity.t()) :: boolean()
@doc """
Relays an activity to a specified peer, determined by the parameters. The
parameters used are controlled by the federation module.
"""
@callback publish_one(Map.t()) :: {:ok, Map.t()} | {:error, any()}
@doc """
Enqueue publishing a single activity.
"""
@spec enqueue_one(module(), Map.t()) :: :ok
def enqueue_one(module, %{} = params),
do: PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_one, module, params])
@spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
def perform(:publish_one, module, params) do
case apply(module, :publish_one, [params]) do
{:ok, _} ->
:ok
{:error, _e} ->
RetryQueue.enqueue(params, module)
end
end
def perform(type, _, _) do
Logger.debug("Unknown task: #{type}")
{:error, "Don't know what to do with this"}
end
@doc """
Relays an activity to all specified peers.
"""
@callback publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok | {:error, any()}
@spec publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok
def publish(%User{} = user, %Activity{} = activity) do
Config.get([:instance, :federation_publisher_modules])
|> Enum.each(fn module ->
if module.is_representable?(activity) do
Logger.info("Publishing #{activity.data["id"]} using #{inspect(module)}")
module.publish(user, activity)
end
end)
:ok
end
@doc """
Gathers links used by an outgoing federation module for WebFinger output.
"""
@callback gather_webfinger_links(Pleroma.User.t()) :: list()
@spec gather_webfinger_links(Pleroma.User.t()) :: list()
def gather_webfinger_links(%User{} = user) do
Config.get([:instance, :federation_publisher_modules])
|> Enum.reduce([], fn module, links ->
links ++ module.gather_webfinger_links(user)
end)
end
@doc """
Gathers nodeinfo protocol names supported by the federation module.
"""
@callback gather_nodeinfo_protocol_names() :: list()
@spec gather_nodeinfo_protocol_names() :: list()
def gather_nodeinfo_protocol_names do
Config.get([:instance, :federation_publisher_modules])
|> Enum.reduce([], fn module, links ->
links ++ module.gather_nodeinfo_protocol_names()
end)
end
end

View file

@ -1,91 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
defmodule Pleroma.Web.HTTPSignatures do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
require Logger
def split_signature(sig) do
default = %{"headers" => "date"}
sig =
sig
|> String.trim()
|> String.split(",")
|> Enum.reduce(default, fn part, acc ->
[key | rest] = String.split(part, "=")
value = Enum.join(rest, "=")
Map.put(acc, key, String.trim(value, "\""))
end)
Map.put(sig, "headers", String.split(sig["headers"], ~r/\s/))
end
def validate(headers, signature, public_key) do
sigstring = build_signing_string(headers, signature["headers"])
Logger.debug("Signature: #{signature["signature"]}")
Logger.debug("Sigstring: #{sigstring}")
{:ok, sig} = Base.decode64(signature["signature"])
:public_key.verify(sigstring, :sha256, sig, public_key)
end
def validate_conn(conn) do
# TODO: How to get the right key and see if it is actually valid for that request.
# For now, fetch the key for the actor.
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
if validate_conn(conn, public_key) do
true
else
Logger.debug("Could not validate, re-fetching user and trying one more time")
# Fetch user anew and try one more time
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
validate_conn(conn, public_key)
end
end
else
_e ->
Logger.debug("Could not public key!")
false
end
end
def validate_conn(conn, public_key) do
headers = Enum.into(conn.req_headers, %{})
signature = split_signature(headers["signature"])
validate(headers, signature, public_key)
end
def build_signing_string(headers, used_headers) do
used_headers
|> Enum.map(fn header -> "#{header}: #{headers[header]}" end)
|> Enum.join("\n")
end
def sign(user, headers) do
with {:ok, %{info: %{keys: keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user),
{:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do
sigstring = build_signing_string(headers, Map.keys(headers))
signature =
:public_key.sign(sigstring, :sha256, private_key)
|> Base.encode64()
[
keyId: user.ap_id <> "#main-key",
algorithm: "rsa-sha256",
headers: Map.keys(headers) |> Enum.join(" "),
signature: signature
]
|> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
|> Enum.join(",")
end
end
end

View file

@ -39,12 +39,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.ControllerHelper alias Pleroma.Web.ControllerHelper
import Ecto.Query import Ecto.Query
require Logger require Logger
plug(
Pleroma.Plugs.RateLimitPlug,
%{
max_requests: Config.get([:app_account_creation, :max_requests]),
interval: Config.get([:app_account_creation, :interval])
}
when action in [:account_register]
)
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
@local_mastodon_name "Mastodon-Local" @local_mastodon_name "Mastodon-Local"
@ -168,7 +178,7 @@ def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
end end
end end
@mastodon_api_level "2.6.5" @mastodon_api_level "2.7.2"
def masto_instance(conn, _params) do def masto_instance(conn, _params) do
instance = Config.get(:instance) instance = Config.get(:instance)
@ -293,7 +303,6 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do
activities = activities =
[user.ap_id | user.following] [user.ap_id | user.following]
|> ActivityPub.fetch_activities(params) |> ActivityPub.fetch_activities(params)
|> ActivityPub.contain_timeline(user)
|> Enum.reverse() |> Enum.reverse()
conn conn
@ -1536,7 +1545,7 @@ def create_filter(
user_id: user.id, user_id: user.id,
phrase: phrase, phrase: phrase,
context: context, context: context,
hide: Map.get(params, "irreversible", nil), hide: Map.get(params, "irreversible", false),
whole_word: Map.get(params, "boolean", true) whole_word: Map.get(params, "boolean", true)
# expires_at # expires_at
} }
@ -1693,6 +1702,53 @@ def reports(%{assigns: %{user: user}} = conn, params) do
end end
end end
def account_register(
%{assigns: %{app: app}} = conn,
%{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
) do
params =
params
|> Map.take([
"email",
"captcha_solution",
"captcha_token",
"captcha_answer_data",
"token",
"password"
])
|> Map.put("nickname", nickname)
|> Map.put("fullname", params["fullname"] || nickname)
|> Map.put("bio", params["bio"] || "")
|> Map.put("confirm", params["password"])
with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
json(conn, %{
token_type: "Bearer",
access_token: token.token,
scope: app.scopes,
created_at: Token.Utils.format_created_at(token)
})
else
{:error, errors} ->
conn
|> put_status(400)
|> json(Jason.encode!(errors))
end
end
def account_register(%{assigns: %{app: _app}} = conn, _params) do
conn
|> put_status(400)
|> json(%{error: "Missing parameters"})
end
def account_register(conn, _) do
conn
|> put_status(403)
|> json(%{error: "Invalid credentials"})
end
def conversations(%{assigns: %{user: user}} = conn, params) do def conversations(%{assigns: %{user: user}} = conn, params) do
participations = Participation.for_user_with_last_activity_id(user, params) participations = Participation.for_user_with_last_activity_id(user, params)

View file

@ -16,6 +16,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]
# TODO: Add cached version. # TODO: Add cached version.
defp get_replied_to_activities(activities) do defp get_replied_to_activities(activities) do
activities activities
@ -340,30 +342,6 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
end end
end end
def get_visibility(object) do
public = "https://www.w3.org/ns/activitystreams#Public"
to = object.data["to"] || []
cc = object.data["cc"] || []
cond do
public in to ->
"public"
public in cc ->
"unlisted"
# this should use the sql for the object's activity
Enum.any?(to, &String.contains?(&1, "/followers")) ->
"private"
length(cc) > 0 ->
"private"
true ->
"direct"
end
end
def render_content(%{data: %{"type" => "Video"}} = object) do def render_content(%{data: %{"type" => "Video"}} = object) do
with name when not is_nil(name) and name != "" <- object.data["name"] do with name when not is_nil(name) and name != "" <- object.data["name"] do
"<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}" "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.Federator.Publisher
plug(Pleroma.Web.FederatingPlug) plug(Pleroma.Web.FederatingPlug)
@ -137,7 +138,7 @@ def raw_nodeinfo do
name: Pleroma.Application.name() |> String.downcase(), name: Pleroma.Application.name() |> String.downcase(),
version: Pleroma.Application.version() version: Pleroma.Application.version()
}, },
protocols: ["ostatus", "activitypub"], protocols: Publisher.gather_nodeinfo_protocol_names(),
services: %{ services: %{
inbound: [], inbound: [],
outbound: [] outbound: []

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.App do
import Ecto.Changeset import Ecto.Changeset
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
schema "apps" do schema "apps" do
field(:client_name, :string) field(:client_name, :string)
field(:redirect_uris, :string) field(:redirect_uris, :string)

View file

@ -14,6 +14,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
import Ecto.Query import Ecto.Query
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
schema "oauth_authorizations" do schema "oauth_authorizations" do
field(:token, :string) field(:token, :string)
field(:scopes, {:array, :string}, default: []) field(:scopes, {:array, :string}, default: [])
@ -25,28 +26,45 @@ defmodule Pleroma.Web.OAuth.Authorization do
timestamps() timestamps()
end end
@spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) ::
{:ok, Authorization.t()} | {:error, Changeset.t()}
def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do 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) scopes: scopes || app.scopes,
authorization = %Authorization{
token: token,
used: false,
user_id: user.id, user_id: user.id,
app_id: app.id, app_id: app.id
scopes: scopes,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
} }
|> create_changeset()
Repo.insert(authorization) |> Repo.insert()
end end
@spec create_changeset(map()) :: Changeset.t()
def create_changeset(attrs \\ %{}) do
%Authorization{}
|> cast(attrs, [:user_id, :app_id, :scopes, :valid_until])
|> validate_required([:app_id, :scopes])
|> add_token()
|> add_lifetime()
end
defp add_token(changeset) do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
put_change(changeset, :token, token)
end
defp add_lifetime(changeset) do
put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10))
end
@spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t()
def use_changeset(%Authorization{} = auth, params) do def use_changeset(%Authorization{} = auth, params) do
auth auth
|> cast(params, [:used]) |> cast(params, [:used])
|> validate_required([:used]) |> validate_required([:used])
end end
@spec use_token(Authorization.t()) ::
{:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()}
def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do
if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do
Repo.update(use_changeset(auth, %{used: true})) Repo.update(use_changeset(auth, %{used: true}))
@ -57,6 +75,7 @@ def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do
def use_token(%Authorization{used: true}), do: {:error, "already used"} def use_token(%Authorization{used: true}), do: {:error, "already used"}
@spec delete_user_authorizations(User.t()) :: {integer(), any()}
def delete_user_authorizations(%User{id: user_id}) do def delete_user_authorizations(%User{id: user_id}) do
from( from(
a in Pleroma.Web.OAuth.Authorization, a in Pleroma.Web.OAuth.Authorization,

View file

@ -19,8 +19,6 @@ defmodule Pleroma.Web.OAuth.OAuthController do
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
@expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)
plug(:fetch_session) plug(:fetch_session)
plug(:fetch_flash) plug(:fetch_flash)
@ -144,14 +142,14 @@ defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do
@doc "Renew access_token with refresh_token" @doc "Renew access_token with refresh_token"
def token_exchange( def token_exchange(
conn, conn,
%{"grant_type" => "refresh_token", "refresh_token" => token} = params %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
) do ) do
with %App{} = app <- get_app_from_request(conn, params), with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do {:ok, token} <- RefreshToken.grant(token) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)} response_attrs = %{created_at: Token.Utils.format_created_at(token)}
json(conn, response_token(user, token, response_attrs)) json(conn, Token.Response.build(user, token, response_attrs))
else else
_error -> _error ->
put_status(conn, 400) put_status(conn, 400)
@ -160,14 +158,14 @@ def token_exchange(
end end
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
with %App{} = app <- get_app_from_request(conn, params), with {:ok, app} <- Token.Utils.fetch_app(conn),
fixed_token = Token.Utils.fix_padding(params["code"]), fixed_token = Token.Utils.fix_padding(params["code"]),
{:ok, auth} <- Authorization.get_by_token(app, fixed_token), {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id), %User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)} response_attrs = %{created_at: Token.Utils.format_created_at(token)}
json(conn, response_token(user, token, response_attrs)) json(conn, Token.Response.build(user, token, response_attrs))
else else
_error -> _error ->
put_status(conn, 400) put_status(conn, 400)
@ -179,14 +177,14 @@ def token_exchange(
conn, conn,
%{"grant_type" => "password"} = params %{"grant_type" => "password"} = params
) do ) do
with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)}, with {:ok, %User{} = user} <- Authenticator.get_user(conn),
%App{} = app <- get_app_from_request(conn, params), {:ok, app} <- Token.Utils.fetch_app(conn),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)}, {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:user_active, true} <- {:user_active, !user.info.deactivated}, {:user_active, true} <- {:user_active, !user.info.deactivated},
{:ok, scopes} <- validate_scopes(app, params), {:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
json(conn, response_token(user, token)) json(conn, Token.Response.build(user, token))
else else
{:auth_active, false} -> {:auth_active, false} ->
# Per https://github.com/tootsuite/mastodon/blob/ # Per https://github.com/tootsuite/mastodon/blob/
@ -218,11 +216,23 @@ def token_exchange(
token_exchange(conn, params) token_exchange(conn, params)
end end
def token_exchange(conn, %{"grant_type" => "client_credentials"} = _params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build_for_client_credentials(token))
else
_error ->
put_status(conn, 400)
|> json(%{error: "Invalid credentials"})
end
end
# Bad request # Bad request
def token_exchange(conn, params), do: bad_request(conn, params) def token_exchange(conn, params), do: bad_request(conn, params)
def token_revoke(conn, %{"token" => _token} = params) do def token_revoke(conn, %{"token" => _token} = params) do
with %App{} = app <- get_app_from_request(conn, params), with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, _token} <- RevokeToken.revoke(app, params) do {:ok, _token} <- RevokeToken.revoke(app, params) do
json(conn, %{}) json(conn, %{})
else else
@ -252,7 +262,7 @@ def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attr
auth_attrs auth_attrs
|> Map.delete("scopes") |> Map.delete("scopes")
|> Map.put("scope", scope) |> Map.put("scope", scope)
|> Poison.encode!() |> Jason.encode!()
params = params =
auth_attrs auth_attrs
@ -316,7 +326,7 @@ def callback(conn, params) do
end end
defp callback_params(%{"state" => state} = params) do defp callback_params(%{"state" => state} = params) do
Map.merge(params, Poison.decode!(state)) Map.merge(params, Jason.decode!(state))
end end
def registration_details(conn, %{"authorization" => auth_attrs}) do def registration_details(conn, %{"authorization" => auth_attrs}) do
@ -405,33 +415,6 @@ defp do_create_authorization(
end end
end end
defp get_app_from_request(conn, params) do
conn
|> fetch_client_credentials(params)
|> fetch_client
end
defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do
Repo.get_by(App, client_id: id, client_secret: secret)
end
defp fetch_client({_id, _secret}), do: nil
defp fetch_client_credentials(conn, params) do
# Per RFC 6749, HTTP Basic is preferred to body params
with ["Basic " <> encoded] <- get_req_header(conn, "authorization"),
{:ok, decoded} <- Base.decode64(encoded),
[id, secret] <-
Enum.map(
String.split(decoded, ":"),
fn s -> URI.decode_www_form(s) end
) do
{id, secret}
else
_ -> {params["client_id"], params["client_secret"]}
end
end
# Special case: Local MastodonFE # Special case: Local MastodonFE
defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login) defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login)
@ -442,18 +425,6 @@ defp get_session_registration_id(conn), do: get_session(conn, :registration_id)
defp put_session_registration_id(conn, registration_id), defp put_session_registration_id(conn, registration_id),
do: put_session(conn, :registration_id, registration_id) do: put_session(conn, :registration_id, registration_id)
defp response_token(%User{} = user, token, opts \\ %{}) do
%{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: @expires_in,
scope: Enum.join(token.scopes, " "),
me: user.ap_id
}
|> Map.merge(opts)
end
@spec validate_scopes(App.t(), map()) :: @spec validate_scopes(App.t(), map()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
defp validate_scopes(app, params) do defp validate_scopes(app, params) do

View file

@ -45,12 +45,16 @@ def get_by_refresh_token(%App{id: app_id} = _app, token) do
|> Repo.find_resource() |> Repo.find_resource()
end end
@spec exchange_token(App.t(), Authorization.t()) ::
{:ok, Token.t()} | {:error, Changeset.t()}
def exchange_token(app, auth) do def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth), with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do true <- auth.app_id == app.id do
user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{}
create_token( create_token(
app, app,
User.get_cached_by_id(auth.user_id), user,
%{scopes: auth.scopes} %{scopes: auth.scopes}
) )
end end
@ -81,12 +85,13 @@ defp put_valid_until(changeset, attrs) do
|> validate_required([:valid_until]) |> validate_required([:valid_until])
end end
@spec create_token(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()}
def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do
%__MODULE__{user_id: user.id, app_id: app.id} %__MODULE__{user_id: user.id, app_id: app.id}
|> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes])
|> validate_required([:scopes, :user_id, :app_id]) |> validate_required([:scopes, :app_id])
|> put_valid_until(attrs) |> put_valid_until(attrs)
|> put_token |> put_token()
|> put_refresh_token(attrs) |> put_refresh_token(attrs)
|> Repo.insert() |> Repo.insert()
end end

View file

@ -0,0 +1,32 @@
defmodule Pleroma.Web.OAuth.Token.Response do
@moduledoc false
alias Pleroma.User
alias Pleroma.Web.OAuth.Token.Utils
@expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)
@doc false
def build(%User{} = user, token, opts \\ %{}) do
%{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: @expires_in,
scope: Enum.join(token.scopes, " "),
me: user.ap_id
}
|> Map.merge(opts)
end
def build_for_client_credentials(token) do
%{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
created_at: Utils.format_created_at(token),
expires_in: @expires_in,
scope: Enum.join(token.scopes, " ")
}
end
end

View file

@ -3,6 +3,44 @@ defmodule Pleroma.Web.OAuth.Token.Utils do
Auxiliary functions for dealing with tokens. Auxiliary functions for dealing with tokens.
""" """
alias Pleroma.Repo
alias Pleroma.Web.OAuth.App
@doc "Fetch app by client credentials from request"
@spec fetch_app(Plug.Conn.t()) :: {:ok, App.t()} | {:error, :not_found}
def fetch_app(conn) do
res =
conn
|> fetch_client_credentials()
|> fetch_client
case res do
%App{} = app -> {:ok, app}
_ -> {:error, :not_found}
end
end
defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do
Repo.get_by(App, client_id: id, client_secret: secret)
end
defp fetch_client({_id, _secret}), do: nil
defp fetch_client_credentials(conn) do
# Per RFC 6749, HTTP Basic is preferred to body params
with ["Basic " <> encoded] <- Plug.Conn.get_req_header(conn, "authorization"),
{:ok, decoded} <- Base.decode64(encoded),
[id, secret] <-
Enum.map(
String.split(decoded, ":"),
fn s -> URI.decode_www_form(s) end
) do
{id, secret}
else
_ -> {conn.params["client_id"], conn.params["client_secret"]}
end
end
@doc "convert token inserted_at to unix timestamp" @doc "convert token inserted_at to unix timestamp"
def format_created_at(%{inserted_at: inserted_at} = _token) do def format_created_at(%{inserted_at: inserted_at} = _token) do
inserted_at inserted_at

View file

@ -16,6 +16,7 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.OStatus.DeleteHandler alias Pleroma.Web.OStatus.DeleteHandler
alias Pleroma.Web.OStatus.FollowHandler alias Pleroma.Web.OStatus.FollowHandler
alias Pleroma.Web.OStatus.NoteHandler alias Pleroma.Web.OStatus.NoteHandler
@ -30,7 +31,7 @@ def is_representable?(%Activity{} = activity) do
is_nil(object) -> is_nil(object) ->
false false
object.data["type"] == "Note" -> Visibility.is_public?(activity) && object.data["type"] == "Note" ->
true true
true -> true ->

View file

@ -34,4 +34,6 @@ def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) d
end end
def fetch_data_for_activity(_), do: %{} def fetch_data_for_activity(_), do: %{}
def perform(:fetch, %Activity{} = activity), do: fetch_data_for_activity(activity)
end end

View file

@ -84,11 +84,13 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.EnsureUserKeyPlug) plug(Pleroma.Plugs.EnsureUserKeyPlug)
end end
pipeline :oauth_read_or_unauthenticated do pipeline :oauth_read_or_public do
plug(Pleroma.Plugs.OAuthScopesPlug, %{ plug(Pleroma.Plugs.OAuthScopesPlug, %{
scopes: ["read"], scopes: ["read"],
fallback: :proceed_unauthenticated fallback: :proceed_unauthenticated
}) })
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
end end
pipeline :oauth_read do pipeline :oauth_read do
@ -192,6 +194,14 @@ defmodule Pleroma.Web.Router do
get("/users", AdminAPIController, :list_users) get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show) get("/users/:nickname", AdminAPIController, :user_show)
get("/reports", AdminAPIController, :list_reports)
get("/reports/:id", AdminAPIController, :report_show)
put("/reports/:id", AdminAPIController, :report_update_state)
post("/reports/:id/respond", AdminAPIController, :report_respond)
put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete)
end end
scope "/", Pleroma.Web.TwitterAPI do scope "/", Pleroma.Web.TwitterAPI do
@ -215,6 +225,7 @@ defmodule Pleroma.Web.Router do
post("/change_password", UtilController, :change_password) post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account) post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings) put("/notification_settings", UtilController, :update_notificaton_settings)
post("/disable_account", UtilController, :disable_account)
end end
scope [] do scope [] do
@ -385,6 +396,8 @@ defmodule Pleroma.Web.Router do
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api) pipe_through(:api)
post("/accounts", MastodonAPIController, :account_register)
get("/instance", MastodonAPIController, :masto_instance) get("/instance", MastodonAPIController, :masto_instance)
get("/instance/peers", MastodonAPIController, :peers) get("/instance/peers", MastodonAPIController, :peers)
post("/apps", MastodonAPIController, :create_app) post("/apps", MastodonAPIController, :create_app)
@ -401,7 +414,7 @@ defmodule Pleroma.Web.Router do
get("/accounts/search", MastodonAPIController, :account_search) get("/accounts/search", MastodonAPIController, :account_search)
scope [] do scope [] do
pipe_through(:oauth_read_or_unauthenticated) pipe_through(:oauth_read_or_public)
get("/timelines/public", MastodonAPIController, :public_timeline) get("/timelines/public", MastodonAPIController, :public_timeline)
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline) get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
@ -422,7 +435,7 @@ defmodule Pleroma.Web.Router do
end end
scope "/api/v2", Pleroma.Web.MastodonAPI do scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through([:api, :oauth_read_or_unauthenticated]) pipe_through([:api, :oauth_read_or_public])
get("/search", MastodonAPIController, :search2) get("/search", MastodonAPIController, :search2)
end end
@ -452,7 +465,7 @@ defmodule Pleroma.Web.Router do
) )
scope [] do scope [] do
pipe_through(:oauth_read_or_unauthenticated) pipe_through(:oauth_read_or_public)
get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline) get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline) get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
@ -470,7 +483,7 @@ defmodule Pleroma.Web.Router do
end end
scope "/api", Pleroma.Web do scope "/api", Pleroma.Web do
pipe_through([:api, :oauth_read_or_unauthenticated]) pipe_through([:api, :oauth_read_or_public])
get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline) get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline)
@ -484,7 +497,7 @@ defmodule Pleroma.Web.Router do
end end
scope "/api", Pleroma.Web, as: :twitter_api_search do scope "/api", Pleroma.Web, as: :twitter_api_search do
pipe_through([:api, :oauth_read_or_unauthenticated]) pipe_through([:api, :oauth_read_or_public])
get("/pleroma/search_user", TwitterAPI.Controller, :search_user) get("/pleroma/search_user", TwitterAPI.Controller, :search_user)
end end
@ -668,7 +681,7 @@ defmodule Pleroma.Web.Router do
delete("/auth/sign_out", MastodonAPIController, :logout) delete("/auth/sign_out", MastodonAPIController, :logout)
scope [] do scope [] do
pipe_through(:oauth_read_or_unauthenticated) pipe_through(:oauth_read_or_public)
get("/web/*path", MastodonAPIController, :index) get("/web/*path", MastodonAPIController, :index)
end end
end end

View file

@ -3,12 +3,18 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Salmon do defmodule Pleroma.Web.Salmon do
@behaviour Pleroma.Web.Federator.Publisher
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
use Bitwise use Bitwise
alias Pleroma.Activity
alias Pleroma.Instances alias Pleroma.Instances
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.ActivityRepresenter alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.XML alias Pleroma.Web.XML
@ -165,12 +171,12 @@ def remote_users(%{data: %{"to" => to} = data}) do
end end
@doc "Pushes an activity to remote account." @doc "Pushes an activity to remote account."
def send_to_user(%{recipient: %{info: %{salmon: salmon}}} = params), def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
do: send_to_user(Map.put(params, :recipient, salmon)) do: publish_one(Map.put(params, :recipient, salmon))
def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is_binary(url) do def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
with {:ok, %{status: code}} when code in 200..299 <- with {:ok, %{status: code}} when code in 200..299 <-
poster.( @httpoison.post(
url, url,
feed, feed,
[{"Content-Type", "application/magic-envelope+xml"}] [{"Content-Type", "application/magic-envelope+xml"}]
@ -184,11 +190,11 @@ def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is
e -> e ->
unless params[:unreachable_since], do: Instances.set_reachable(url) unless params[:unreachable_since], do: Instances.set_reachable(url)
Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end) Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
:error {:error, "Unreachable instance"}
end end
end end
def send_to_user(_), do: :noop def publish_one(_), do: :noop
@supported_activities [ @supported_activities [
"Create", "Create",
@ -199,13 +205,19 @@ def send_to_user(_), do: :noop
"Delete" "Delete"
] ]
def is_representable?(%Activity{data: %{"type" => type}} = activity)
when type in @supported_activities,
do: Visibility.is_public?(activity)
def is_representable?(_), do: false
@doc """ @doc """
Publishes an activity to remote accounts Publishes an activity to remote accounts
""" """
@spec publish(User.t(), Pleroma.Activity.t(), Pleroma.HTTP.t()) :: none @spec publish(User.t(), Pleroma.Activity.t()) :: none
def publish(user, activity, poster \\ &@httpoison.post/3) def publish(user, activity)
def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity, poster) def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity)
when type in @supported_activities do when type in @supported_activities do
feed = ActivityRepresenter.to_simple_form(activity, user, true) feed = ActivityRepresenter.to_simple_form(activity, user, true)
@ -229,15 +241,29 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity
|> Enum.each(fn remote_user -> |> Enum.each(fn remote_user ->
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end) Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
Pleroma.Web.Federator.publish_single_salmon(%{ Publisher.enqueue_one(__MODULE__, %{
recipient: remote_user, recipient: remote_user,
feed: feed, feed: feed,
poster: poster,
unreachable_since: reachable_urls_metadata[remote_user.info.salmon] unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
}) })
end) end)
end end
end end
def publish(%{id: id}, _, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end) def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
def gather_webfinger_links(%User{} = user) do
{:ok, _private, public} = keys_from_pem(user.info.keys)
magic_key = encode_key(public)
[
%{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
%{
"rel" => "magic-public-key",
"href" => "data:application/magic-public-key,#{magic_key}"
}
]
end
def gather_nodeinfo_protocol_names, do: []
end end

View file

@ -173,8 +173,6 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_
def config(conn, _params) do def config(conn, _params) do
instance = Pleroma.Config.get(:instance) instance = Pleroma.Config.get(:instance)
instance_fe = Pleroma.Config.get(:fe)
instance_chat = Pleroma.Config.get(:chat)
case get_format(conn) do case get_format(conn) do
"xml" -> "xml" ->
@ -219,31 +217,7 @@ def config(conn, _params) do
if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0") if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0")
} }
pleroma_fe = pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
if instance_fe do
%{
theme: Keyword.get(instance_fe, :theme),
background: Keyword.get(instance_fe, :background),
logo: Keyword.get(instance_fe, :logo),
logoMask: Keyword.get(instance_fe, :logo_mask),
logoMargin: Keyword.get(instance_fe, :logo_margin),
redirectRootNoLogin: Keyword.get(instance_fe, :redirect_root_no_login),
redirectRootLogin: Keyword.get(instance_fe, :redirect_root_login),
chatDisabled: !Keyword.get(instance_chat, :enabled),
showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel),
scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled),
formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled),
collapseMessageWithSubject:
Keyword.get(instance_fe, :collapse_message_with_subject),
hidePostStats: Keyword.get(instance_fe, :hide_post_stats),
hideUserStats: Keyword.get(instance_fe, :hide_user_stats),
scopeCopy: Keyword.get(instance_fe, :scope_copy),
subjectLineBehavior: Keyword.get(instance_fe, :subject_line_behavior),
alwaysShowSubjectInput: Keyword.get(instance_fe, :always_show_subject_input)
}
else
Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
end
managed_config = Keyword.get(instance, :managed_config) managed_config = Keyword.get(instance, :managed_config)
@ -309,8 +283,13 @@ def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do
Enum.map(lines, fn line -> Enum.map(lines, fn line ->
String.split(line, ",") |> List.first() String.split(line, ",") |> List.first()
end) end)
|> List.delete("Account address"), |> List.delete("Account address") do
{:ok, _} = Task.start(fn -> User.follow_import(follower, followed_identifiers) end) do PleromaJobQueue.enqueue(:background, User, [
:follow_import,
follower,
followed_identifiers
])
json(conn, "job started") json(conn, "job started")
end end
end end
@ -320,8 +299,13 @@ def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
end end
def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do
with blocked_identifiers <- String.split(list), with blocked_identifiers <- String.split(list) do
{:ok, _} = Task.start(fn -> User.blocks_import(blocker, blocked_identifiers) end) do PleromaJobQueue.enqueue(:background, User, [
:blocks_import,
blocker,
blocked_identifiers
])
json(conn, "job started") json(conn, "job started")
end end
end end
@ -360,6 +344,17 @@ def delete_account(%{assigns: %{user: user}} = conn, params) do
end end
end end
def disable_account(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
{:ok, user} ->
User.deactivate_async(user)
json(conn, %{status: "success"})
{:error, msg} ->
json(conn, %{error: msg})
end
end
def captcha(conn, _params) do def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new()) json(conn, Pleroma.Captcha.new())
end end

View file

@ -128,7 +128,7 @@ def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
end end
end end
def register_user(params) do def register_user(params, opts \\ []) do
token = params["token"] token = params["token"]
params = %{ params = %{
@ -162,13 +162,22 @@ def register_user(params) do
# I have no idea how this error handling works # I have no idea how this error handling works
{:error, %{error: Jason.encode!(%{captcha: [error]})}} {:error, %{error: Jason.encode!(%{captcha: [error]})}}
else else
registrations_open = Pleroma.Config.get([:instance, :registrations_open]) registration_process(
registration_process(registrations_open, params, token) params,
%{
registrations_open: Pleroma.Config.get([:instance, :registrations_open]),
token: token
},
opts
)
end end
end end
defp registration_process(registration_open, params, token) defp registration_process(params, %{registrations_open: true}, opts) do
when registration_open == false or is_nil(registration_open) do create_user(params, opts)
end
defp registration_process(params, %{token: token}, opts) do
invite = invite =
unless is_nil(token) do unless is_nil(token) do
Repo.get_by(UserInviteToken, %{token: token}) Repo.get_by(UserInviteToken, %{token: token})
@ -182,19 +191,15 @@ defp registration_process(registration_open, params, token)
invite when valid_invite? -> invite when valid_invite? ->
UserInviteToken.update_usage!(invite) UserInviteToken.update_usage!(invite)
create_user(params) create_user(params, opts)
_ -> _ ->
{:error, "Expired token"} {:error, "Expired token"}
end end
end end
defp registration_process(true, params, _token) do defp create_user(params, opts) do
create_user(params) changeset = User.register_changeset(%User{}, params, opts)
end
defp create_user(params) do
changeset = User.register_changeset(%User{}, params)
case User.register(changeset) do case User.register(changeset) do
{:ok, user} -> {:ok, user} ->
@ -231,12 +236,15 @@ def password_reset(nickname_or_email) do
def get_user(user \\ nil, params) do def get_user(user \\ nil, params) do
case params do case params do
%{"user_id" => user_id} -> %{"user_id" => user_id} ->
case target = User.get_cached_by_nickname_or_id(user_id) do case User.get_cached_by_nickname_or_id(user_id) do
nil -> nil ->
{:error, "No user with such user_id"} {:error, "No user with such user_id"}
_ -> %User{info: %{deactivated: true}} ->
{:ok, target} {:error, "User has been disabled"}
user ->
{:ok, user}
end end
%{"screen_name" => nickname} -> %{"screen_name" => nickname} ->

View file

@ -101,9 +101,7 @@ def friends_timeline(%{assigns: %{user: user}} = conn, params) do
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("user", user) |> Map.put("user", user)
activities = activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> ActivityPub.contain_timeline(user)
conn conn
|> put_view(ActivityView) |> put_view(ActivityView)
@ -440,7 +438,7 @@ def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
true <- user.local, true <- user.local,
true <- user.info.confirmation_pending, true <- user.info.confirmation_pending,
true <- user.info.confirmation_token == token, true <- user.info.confirmation_token == token,
info_change <- User.Info.confirmation_changeset(user.info, :confirmed), info_change <- User.Info.confirmation_changeset(user.info, need_confirmation: false),
changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change), changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
{:ok, _} <- User.update_and_set_cache(changeset) do {:ok, _} <- User.update_and_set_cache(changeset) do
conn conn

View file

@ -310,7 +310,7 @@ def render(
"tags" => tags, "tags" => tags,
"activity_type" => "post", "activity_type" => "post",
"possibly_sensitive" => possibly_sensitive, "possibly_sensitive" => possibly_sensitive,
"visibility" => StatusView.get_visibility(object), "visibility" => Pleroma.Web.ActivityPub.Visibility.get_visibility(object),
"summary" => summary, "summary" => summary,
"summary_html" => summary |> Formatter.emojify(object.data["emoji"]), "summary_html" => summary |> Formatter.emojify(object.data["emoji"]),
"card" => card, "card" => card,

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.WebFinger do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.OStatus alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Salmon alias Pleroma.Web.Salmon
alias Pleroma.Web.XML alias Pleroma.Web.XML
alias Pleroma.XmlBuilder alias Pleroma.XmlBuilder
@ -50,70 +50,40 @@ def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
end end
end end
defp gather_links(%User{} = user) do
[
%{
"rel" => "http://webfinger.net/rel/profile-page",
"type" => "text/html",
"href" => user.ap_id
}
] ++ Publisher.gather_webfinger_links(user)
end
def represent_user(user, "JSON") do def represent_user(user, "JSON") do
{:ok, user} = ensure_keys_present(user) {:ok, user} = ensure_keys_present(user)
{:ok, _private, public} = Salmon.keys_from_pem(user.info.keys)
magic_key = Salmon.encode_key(public)
%{ %{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
"aliases" => [user.ap_id], "aliases" => [user.ap_id],
"links" => [ "links" => gather_links(user)
%{
"rel" => "http://schemas.google.com/g/2010#updates-from",
"type" => "application/atom+xml",
"href" => OStatus.feed_path(user)
},
%{
"rel" => "http://webfinger.net/rel/profile-page",
"type" => "text/html",
"href" => user.ap_id
},
%{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
%{
"rel" => "magic-public-key",
"href" => "data:application/magic-public-key,#{magic_key}"
},
%{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
%{
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href" => user.ap_id
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => OStatus.remote_follow_path()
}
]
} }
end end
def represent_user(user, "XML") do def represent_user(user, "XML") do
{:ok, user} = ensure_keys_present(user) {:ok, user} = ensure_keys_present(user)
{:ok, _private, public} = Salmon.keys_from_pem(user.info.keys)
magic_key = Salmon.encode_key(public) links =
gather_links(user)
|> Enum.map(fn link -> {:Link, link} end)
{ {
:XRD, :XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[ [
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"}, {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"},
{:Alias, user.ap_id}, {:Alias, user.ap_id}
{:Link, ] ++ links
%{
rel: "http://schemas.google.com/g/2010#updates-from",
type: "application/atom+xml",
href: OStatus.feed_path(user)
}},
{:Link,
%{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}},
{:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}},
{:Link,
%{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}},
{:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}},
{:Link,
%{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}}
]
} }
|> XmlBuilder.to_doc() |> XmlBuilder.to_doc()
end end

View file

@ -4,10 +4,14 @@
defmodule Pleroma.Web.Websub do defmodule Pleroma.Web.Websub do
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Instances alias Pleroma.Instances
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.FeedRepresenter alias Pleroma.Web.OStatus.FeedRepresenter
alias Pleroma.Web.Router.Helpers alias Pleroma.Web.Router.Helpers
@ -18,6 +22,8 @@ defmodule Pleroma.Web.Websub do
import Ecto.Query import Ecto.Query
@behaviour Pleroma.Web.Federator.Publisher
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
def verify(subscription, getter \\ &@httpoison.get/3) do def verify(subscription, getter \\ &@httpoison.get/3) do
@ -56,6 +62,13 @@ def verify(subscription, getter \\ &@httpoison.get/3) do
"Undo", "Undo",
"Delete" "Delete"
] ]
def is_representable?(%Activity{data: %{"type" => type}} = activity)
when type in @supported_activities,
do: Visibility.is_public?(activity)
def is_representable?(_), do: false
def publish(topic, user, %{data: %{"type" => type}} = activity) def publish(topic, user, %{data: %{"type" => type}} = activity)
when type in @supported_activities do when type in @supported_activities do
response = response =
@ -88,12 +101,14 @@ def publish(topic, user, %{data: %{"type" => type}} = activity)
unreachable_since: reachable_callbacks_metadata[sub.callback] unreachable_since: reachable_callbacks_metadata[sub.callback]
} }
Federator.publish_single_websub(data) Publisher.enqueue_one(__MODULE__, data)
end) end)
end end
def publish(_, _, _), do: "" def publish(_, _, _), do: ""
def publish(actor, activity), do: publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
def sign(secret, doc) do def sign(secret, doc) do
:crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase() :crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase()
end end
@ -299,4 +314,20 @@ def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret} =
{:error, response} {:error, response}
end end
end end
def gather_webfinger_links(%User{} = user) do
[
%{
"rel" => "http://schemas.google.com/g/2010#updates-from",
"type" => "application/atom+xml",
"href" => OStatus.feed_path(user)
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => OStatus.remote_follow_path()
}
]
end
def gather_nodeinfo_protocol_names, do: ["ostatus"]
end end

View file

@ -35,6 +35,7 @@ def to_doc(content), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(co
defp make_open_tag(tag, attributes) do defp make_open_tag(tag, attributes) do
attributes_string = attributes_string =
for {attribute, value} <- attributes do for {attribute, value} <- attributes do
value = String.replace(value, "\"", "&quot;")
"#{attribute}=\"#{value}\"" "#{attribute}=\"#{value}\""
end end
|> Enum.join(" ") |> Enum.join(" ")

View file

@ -13,6 +13,7 @@ def project do
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
aliases: aliases(), aliases: aliases(),
deps: deps(), deps: deps(),
test_coverage: [tool: ExCoveralls],
# Docs # Docs
name: "Pleroma", name: "Pleroma",
@ -103,6 +104,9 @@ defp deps do
{:auto_linker, {:auto_linker,
git: "https://git.pleroma.social/pleroma/auto_linker.git", git: "https://git.pleroma.social/pleroma/auto_linker.git",
ref: "c00c4e75b35367fa42c95ffd9b8c455bf9995829"}, ref: "c00c4e75b35367fa42c95ffd9b8c455bf9995829"},
{:http_signatures,
git: "https://git.pleroma.social/pleroma/http_signatures.git",
ref: "9789401987096ead65646b52b5a2ca6bf52fc531"},
{:pleroma_job_queue, "~> 0.2.0"}, {:pleroma_job_queue, "~> 0.2.0"},
{:telemetry, "~> 0.3"}, {:telemetry, "~> 0.3"},
{:prometheus_ex, "~> 3.0"}, {:prometheus_ex, "~> 3.0"},
@ -114,7 +118,9 @@ defp deps do
{:quack, "~> 0.1.1"}, {:quack, "~> 0.1.1"},
{:benchee, "~> 1.0"}, {:benchee, "~> 1.0"},
{:esshd, "~> 0.1.0"}, {:esshd, "~> 0.1.0"},
{:plug_static_index_html, "~> 1.0.0"} {:ex_rated, "~> 1.2"},
{:plug_static_index_html, "~> 1.0.0"},
{:excoveralls, "~> 0.11.1", only: :test}
] ++ oauth_deps ] ++ oauth_deps
end end

View file

@ -24,17 +24,21 @@
"ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
"esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"}, "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"},
"eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"}, "eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"},
"ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
"ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"}, "ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"}, "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"},
"ex_rated": {:hex, :ex_rated, "1.3.2", "6aeb32abb46ea6076f417a9ce8cb1cf08abf35fb2d42375beaad4dd72b550bf1", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"},
"ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]}, "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]},
"excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
"gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"},
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"}, "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "9789401987096ead65646b52b5a2ca6bf52fc531", [ref: "9789401987096ead65646b52b5a2ca6bf52fc531"]},
"httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},

View file

@ -0,0 +1,7 @@
defmodule Pleroma.Repo.Migrations.AddIndexOnUserInfoDeactivated do
use Ecto.Migration
def change do
create(index(:users, ["(info->'deactivated')"], name: :users_deactivated_index, using: :gin))
end
end

View file

@ -0,0 +1,19 @@
defmodule Pleroma.Repo.Migrations.SetDefaultStateToReports do
use Ecto.Migration
def up do
execute """
UPDATE activities AS a
SET data = jsonb_set(data, '{state}', '"open"', true)
WHERE data->>'type' = 'Flag'
"""
end
def down do
execute """
UPDATE activities AS a
SET data = data #- '{state}'
WHERE data->>'type' = 'Flag'
"""
end
end

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.ChangeHideColumnInFilterTable do
use Ecto.Migration
def change do
alter table(:filters) do
modify :hide, :boolean, default: false
end
end
end

View file

@ -0,0 +1,73 @@
defmodule Pleroma.Repo.Migrations.AddThreadVisibilityFunction do
use Ecto.Migration
@disable_ddl_transaction true
def up do
statement = """
CREATE OR REPLACE FUNCTION thread_visibility(actor varchar, activity_id varchar) RETURNS boolean AS $$
DECLARE
public varchar := 'https://www.w3.org/ns/activitystreams#Public';
child objects%ROWTYPE;
activity activities%ROWTYPE;
actor_user users%ROWTYPE;
author_fa varchar;
valid_recipients varchar[];
BEGIN
--- Fetch our actor.
SELECT * INTO actor_user FROM users WHERE users.ap_id = actor;
--- Fetch our initial activity.
SELECT * INTO activity FROM activities WHERE activities.data->>'id' = activity_id;
LOOP
--- Ensure that we have an activity before continuing.
--- If we don't, the thread is not satisfiable.
IF activity IS NULL THEN
RETURN false;
END IF;
--- We only care about Create activities.
IF activity.data->>'type' != 'Create' THEN
RETURN true;
END IF;
--- Normalize the child object into child.
SELECT * INTO child FROM objects
INNER JOIN activities ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
WHERE COALESCE(activity.data->'object'->>'id', activity.data->>'object') = objects.data->>'id';
--- Fetch the author's AS2 following collection.
SELECT COALESCE(users.follower_address, '') INTO author_fa FROM users WHERE users.ap_id = activity.actor;
--- Prepare valid recipients array.
valid_recipients := ARRAY[actor, public];
IF ARRAY[author_fa] && actor_user.following THEN
valid_recipients := valid_recipients || author_fa;
END IF;
--- Check visibility.
IF NOT valid_recipients && activity.recipients THEN
--- activity not visible, break out of the loop
RETURN false;
END IF;
--- If there's a parent, load it and do this all over again.
IF (child.data->'inReplyTo' IS NOT NULL) AND (child.data->'inReplyTo' != 'null'::jsonb) THEN
SELECT * INTO activity FROM activities
INNER JOIN objects ON COALESCE(activities.data->'object'->>'id', activities.data->>'object') = objects.data->>'id'
WHERE child.data->>'inReplyTo' = objects.data->>'id';
ELSE
RETURN true;
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
"""
execute(statement)
end
def down do
execute("drop function thread_visibility(actor varchar, activity_id varchar)")
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,014 KiB

View file

@ -28,6 +28,15 @@ test "get/1 with a list of keys" do
assert Pleroma.Config.get([:azerty, :uiop], true) == true assert Pleroma.Config.get([:azerty, :uiop], true) == true
end end
test "get/1 when value is false" do
Pleroma.Config.put([:instance, :false_test], false)
Pleroma.Config.put([:instance, :nested], [])
Pleroma.Config.put([:instance, :nested, :false_test], false)
assert Pleroma.Config.get([:instance, :false_test]) == false
assert Pleroma.Config.get([:instance, :nested, :false_test]) == false
end
test "get!/1" do test "get!/1" do
assert Pleroma.Config.get!(:instance) == Application.get_env(:pleroma, :instance) assert Pleroma.Config.get!(:instance) == Application.get_env(:pleroma, :instance)
@ -43,6 +52,15 @@ test "get!/1" do
end) end)
end end
test "get!/1 when value is false" do
Pleroma.Config.put([:instance, :false_test], false)
Pleroma.Config.put([:instance, :nested], [])
Pleroma.Config.put([:instance, :nested, :false_test], false)
assert Pleroma.Config.get!([:instance, :false_test]) == false
assert Pleroma.Config.get!([:instance, :nested, :false_test]) == false
end
test "put/2 with a key" do test "put/2 with a key" do
Pleroma.Config.put(:config_test, true) Pleroma.Config.put(:config_test, true)

View file

@ -11,6 +11,26 @@ defmodule Pleroma.ConversationTest do
import Pleroma.Factory import Pleroma.Factory
test "it goes through old direct conversations" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{"visibility" => "direct", "status" => "hey @#{other_user.nickname}"})
Repo.delete_all(Conversation)
Repo.delete_all(Conversation.Participation)
refute Repo.one(Conversation)
Conversation.bump_for_all_activities()
assert Repo.one(Conversation)
[participation, _p2] = Repo.all(Conversation.Participation)
assert participation.read
end
test "it creates a conversation for given ap_id" do test "it creates a conversation for given ap_id" do
assert {:ok, %Conversation{} = conversation} = assert {:ok, %Conversation{} = conversation} =
Conversation.create_for_ap_id("https://some_ap_id") Conversation.create_for_ap_id("https://some_ap_id")

View file

@ -0,0 +1,55 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Config
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.User
test "it halts if not public and no user is assigned", %{conn: conn} do
set_public_to(false)
conn =
conn
|> EnsurePublicOrAuthenticatedPlug.call(%{})
assert conn.status == 403
assert conn.halted == true
end
test "it continues if public", %{conn: conn} do
set_public_to(true)
ret_conn =
conn
|> EnsurePublicOrAuthenticatedPlug.call(%{})
assert ret_conn == conn
end
test "it continues if a user is assigned, even if not public", %{conn: conn} do
set_public_to(false)
conn =
conn
|> assign(:user, %User{})
ret_conn =
conn
|> EnsurePublicOrAuthenticatedPlug.call(%{})
assert ret_conn == conn
end
defp set_public_to(value) do
orig = Config.get!([:instance, :public])
Config.put([:instance, :public], value)
on_exit(fn ->
Config.put([:instance, :public], orig)
end)
end
end

View file

@ -4,7 +4,6 @@
defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Web.HTTPSignatures
alias Pleroma.Web.Plugs.HTTPSignaturePlug alias Pleroma.Web.Plugs.HTTPSignaturePlug
import Plug.Conn import Plug.Conn

View file

@ -0,0 +1,50 @@
defmodule Pleroma.Plugs.RateLimitPlugTest do
use ExUnit.Case, async: true
use Plug.Test
alias Pleroma.Plugs.RateLimitPlug
@opts RateLimitPlug.init(%{max_requests: 5, interval: 1})
setup do
enabled = Pleroma.Config.get([:app_account_creation, :enabled])
Pleroma.Config.put([:app_account_creation, :enabled], true)
on_exit(fn ->
Pleroma.Config.put([:app_account_creation, :enabled], enabled)
end)
:ok
end
test "it restricts by opts" do
conn = conn(:get, "/")
bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".")
ms = 1000
conn = RateLimitPlug.call(conn, @opts)
{1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
conn = RateLimitPlug.call(conn, @opts)
{2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
conn = RateLimitPlug.call(conn, @opts)
{3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
conn = RateLimitPlug.call(conn, @opts)
{4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
conn = RateLimitPlug.call(conn, @opts)
{5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
conn = RateLimitPlug.call(conn, @opts)
assert conn.status == 403
assert conn.halted
assert conn.resp_body == "{\"error\":\"Rate limit exceeded.\"}"
Process.sleep(to_reset)
conn = conn(:get, "/")
conn = RateLimitPlug.call(conn, @opts)
{1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5)
refute conn.status == 403
refute conn.halted
refute conn.resp_body
end
end

View file

@ -43,7 +43,7 @@ def user_factory do
def note_factory(attrs \\ %{}) do def note_factory(attrs \\ %{}) do
text = sequence(:text, &"This is :moominmamma: note #{&1}") text = sequence(:text, &"This is :moominmamma: note #{&1}")
user = insert(:user) user = attrs[:user] || insert(:user)
data = %{ data = %{
"type" => "Note", "type" => "Note",
@ -113,7 +113,8 @@ def direct_note_activity_factory do
end end
def note_activity_factory(attrs \\ %{}) do def note_activity_factory(attrs \\ %{}) do
note = attrs[:note] || insert(:note) user = attrs[:user] || insert(:user)
note = attrs[:note] || insert(:note, user: user)
data = %{ data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),

View file

@ -338,4 +338,31 @@ test "activities are deleted" do
assert message == "User #{nickname} statuses deleted." assert message == "User #{nickname} statuses deleted."
end end
end end
describe "running toggle_confirmed" do
test "user is confirmed" do
%{id: id, nickname: nickname} = insert(:user, info: %{confirmation_pending: false})
assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname])
assert_received {:mix_shell, :info, [message]}
assert message == "#{nickname} needs confirmation."
user = Repo.get(User, id)
assert user.info.confirmation_pending
assert user.info.confirmation_token
end
test "user is not confirmed" do
%{id: id, nickname: nickname} =
insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"})
assert :ok = Mix.Tasks.Pleroma.User.run(["toggle_confirmed", nickname])
assert_received {:mix_shell, :info, [message]}
assert message == "#{nickname} doesn't need confirmation."
user = Repo.get(User, id)
refute user.info.confirmation_pending
refute user.info.confirmation_token
end
end
end end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.UserTest do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
use Pleroma.DataCase use Pleroma.DataCase
@ -213,8 +214,8 @@ test "test if a user is following another user" do
test "fetches correct profile for nickname beginning with number" do test "fetches correct profile for nickname beginning with number" do
# Use old-style integer ID to try to reproduce the problem # Use old-style integer ID to try to reproduce the problem
user = insert(:user, %{id: 1080}) user = insert(:user, %{id: 1080})
userwithnumbers = insert(:user, %{nickname: "#{user.id}garbage"}) user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"})
assert userwithnumbers == User.get_cached_by_nickname_or_id(userwithnumbers.nickname) assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname)
end end
describe "user registration" do describe "user registration" do
@ -349,7 +350,7 @@ test "it creates unconfirmed user" do
end end
test "it creates confirmed user if :confirmed option is given" do test "it creates confirmed user if :confirmed option is given" do
changeset = User.register_changeset(%User{}, @full_user_data, confirmed: true) changeset = User.register_changeset(%User{}, @full_user_data, need_confirmation: false)
assert changeset.valid? assert changeset.valid?
{:ok, user} = Repo.insert(changeset) {:ok, user} = Repo.insert(changeset)
@ -816,13 +817,71 @@ test "get recipients from activity" do
assert addressed in recipients assert addressed in recipients
end end
test ".deactivate can de-activate then re-activate a user" do describe ".deactivate" do
user = insert(:user) test "can de-activate then re-activate a user" do
assert false == user.info.deactivated user = insert(:user)
{:ok, user} = User.deactivate(user) assert false == user.info.deactivated
assert true == user.info.deactivated {:ok, user} = User.deactivate(user)
{:ok, user} = User.deactivate(user, false) assert true == user.info.deactivated
assert false == user.info.deactivated {:ok, user} = User.deactivate(user, false)
assert false == user.info.deactivated
end
test "hide a user from followers " do
user = insert(:user)
user2 = insert(:user)
{:ok, user} = User.follow(user, user2)
{:ok, _user} = User.deactivate(user)
info = User.get_cached_user_info(user2)
assert info.follower_count == 0
assert {:ok, []} = User.get_followers(user2)
end
test "hide a user from friends" do
user = insert(:user)
user2 = insert(:user)
{:ok, user2} = User.follow(user2, user)
assert User.following_count(user2) == 1
{:ok, _user} = User.deactivate(user)
info = User.get_cached_user_info(user2)
assert info.following_count == 0
assert User.following_count(user2) == 0
assert {:ok, []} = User.get_friends(user2)
end
test "hide a user's statuses from timelines and notifications" do
user = insert(:user)
user2 = insert(:user)
{:ok, user2} = User.follow(user2, user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}"})
activity = Repo.preload(activity, :bookmark)
[notification] = Pleroma.Notification.for_user(user2)
assert notification.activity.id == activity.id
assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark)
assert [activity] ==
ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
{:ok, _user} = User.deactivate(user)
assert [] == ActivityPub.fetch_public_activities(%{})
assert [] == Pleroma.Notification.for_user(user2)
assert [] ==
ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
end
end end
test ".delete_user_activities deletes all create activities" do test ".delete_user_activities deletes all create activities" do
@ -1143,4 +1202,22 @@ test "follower count is updated when a follower is blocked" do
assert Map.get(user_show, "followers_count") == 2 assert Map.get(user_show, "followers_count") == 2
end end
describe "toggle_confirmation/1" do
test "if user is confirmed" do
user = insert(:user, info: %{confirmation_pending: false})
{:ok, user} = User.toggle_confirmation(user)
assert user.info.confirmation_pending
assert user.info.confirmation_token
end
test "if user is unconfirmed" do
user = insert(:user, info: %{confirmation_pending: true, confirmation_token: "some token"})
{:ok, user} = User.toggle_confirmation(user)
refute user.info.confirmation_pending
refute user.info.confirmation_token
end
end
end end

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -461,6 +462,29 @@ test "doesn't return announce activities concerning blocked users" do
refute Enum.member?(activities, activity_three.id) refute Enum.member?(activities, activity_three.id)
end end
test "doesn't return activities from blocked domains" do
domain = "dogwhistle.zone"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
activity = insert(:note_activity, %{note: note})
user = insert(:user)
{:ok, user} = User.block_domain(user, domain)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute activity in activities
followed_user = insert(:user)
ActivityPub.follow(user, followed_user)
{:ok, repeat_activity, _} = CommonAPI.repeat(activity.id, followed_user)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute repeat_activity in activities
end
test "doesn't return muted activities" do test "doesn't return muted activities" do
activity_one = insert(:note_activity) activity_one = insert(:note_activity)
activity_two = insert(:note_activity) activity_two = insert(:note_activity)
@ -959,18 +983,21 @@ test "it filters broken threads" do
"in_reply_to_status_id" => private_activity_2.id "in_reply_to_status_id" => private_activity_2.id
}) })
activities = ActivityPub.fetch_activities([user1.ap_id | user1.following]) activities =
ActivityPub.fetch_activities([user1.ap_id | user1.following])
|> Enum.map(fn a -> a.id end)
private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"]) private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
assert [public_activity, private_activity_1, private_activity_3] == assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities
activities
assert length(activities) == 3 assert length(activities) == 3
activities = ActivityPub.contain_timeline(activities, user1) activities =
ActivityPub.fetch_activities([user1.ap_id | user1.following], %{"user" => user1})
|> Enum.map(fn a -> a.id end)
assert [public_activity, private_activity_1] == activities assert [public_activity.id, private_activity_1.id] == activities
assert length(activities) == 2 assert length(activities) == 2
end end
end end
@ -1057,7 +1084,7 @@ test "it can create a Flag activity" do
actor = insert(:user) actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_reachable(inbox)) assert called(Instances.set_reachable(inbox))
end end
@ -1070,7 +1097,7 @@ test "it can create a Flag activity" do
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = assert {:ok, _} =
ActivityPub.publish_one(%{ Publisher.publish_one(%{
inbox: inbox, inbox: inbox,
json: "{}", json: "{}",
actor: actor, actor: actor,
@ -1089,7 +1116,7 @@ test "it can create a Flag activity" do
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = assert {:ok, _} =
ActivityPub.publish_one(%{ Publisher.publish_one(%{
inbox: inbox, inbox: inbox,
json: "{}", json: "{}",
actor: actor, actor: actor,
@ -1107,8 +1134,7 @@ test "it can create a Flag activity" do
actor = insert(:user) actor = insert(:user)
inbox = "http://404.site/users/nick1/inbox" inbox = "http://404.site/users/nick1/inbox"
assert {:error, _} = assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_unreachable(inbox)) assert called(Instances.set_unreachable(inbox))
end end
@ -1120,8 +1146,7 @@ test "it can create a Flag activity" do
actor = insert(:user) actor = insert(:user)
inbox = "http://connrefused.site/users/nick1/inbox" inbox = "http://connrefused.site/users/nick1/inbox"
assert {:error, _} = assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_unreachable(inbox)) assert called(Instances.set_unreachable(inbox))
end end
@ -1133,7 +1158,7 @@ test "it can create a Flag activity" do
actor = insert(:user) actor = insert(:user)
inbox = "http://200.site/users/nick1/inbox" inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
refute called(Instances.set_unreachable(inbox)) refute called(Instances.set_unreachable(inbox))
end end
@ -1146,7 +1171,7 @@ test "it can create a Flag activity" do
inbox = "http://connrefused.site/users/nick1/inbox" inbox = "http://connrefused.site/users/nick1/inbox"
assert {:error, _} = assert {:error, _} =
ActivityPub.publish_one(%{ Publisher.publish_one(%{
inbox: inbox, inbox: inbox,
json: "{}", json: "{}",
actor: actor, actor: actor,

View file

@ -95,4 +95,26 @@ test "visible_for_user?", %{
refute Visibility.visible_for_user?(private, unrelated) refute Visibility.visible_for_user?(private, unrelated)
refute Visibility.visible_for_user?(direct, unrelated) refute Visibility.visible_for_user?(direct, unrelated)
end end
test "doesn't die when the user doesn't exist",
%{
direct: direct,
user: user
} do
Repo.delete(user)
Cachex.clear(:user_cache)
refute Visibility.is_private?(direct)
end
test "get_visibility", %{
public: public,
private: private,
direct: direct,
unlisted: unlisted
} do
assert Visibility.get_visibility(public) == "public"
assert Visibility.get_visibility(private) == "private"
assert Visibility.get_visibility(direct) == "direct"
assert Visibility.get_visibility(unlisted) == "unlisted"
end
end end

View file

@ -5,8 +5,10 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Activity
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
describe "/api/pleroma/admin/users" do describe "/api/pleroma/admin/users" do
@ -949,4 +951,329 @@ test "with token" do
} }
end end
end end
describe "GET /api/pleroma/admin/reports/:id" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
%{conn: assign(conn, :user, admin)}
end
test "returns report by its id", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
response =
conn
|> get("/api/pleroma/admin/reports/#{report_id}")
|> json_response(:ok)
assert response["id"] == report_id
end
test "returns 404 when report id is invalid", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/reports/test")
assert json_response(conn, :not_found) == "Not found"
end
end
describe "PUT /api/pleroma/admin/reports/:id" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
%{conn: assign(conn, :user, admin), id: report_id}
end
test "mark report as resolved", %{conn: conn, id: id} do
response =
conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "resolved"})
|> json_response(:ok)
assert response["state"] == "resolved"
end
test "closes report", %{conn: conn, id: id} do
response =
conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "closed"})
|> json_response(:ok)
assert response["state"] == "closed"
end
test "returns 400 when state is unknown", %{conn: conn, id: id} do
conn =
conn
|> put("/api/pleroma/admin/reports/#{id}", %{"state" => "test"})
assert json_response(conn, :bad_request) == "Unsupported state"
end
test "returns 404 when report is not exist", %{conn: conn} do
conn =
conn
|> put("/api/pleroma/admin/reports/test", %{"state" => "closed"})
assert json_response(conn, :not_found) == "Not found"
end
end
describe "GET /api/pleroma/admin/reports" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
%{conn: assign(conn, :user, admin)}
end
test "returns empty response when no reports created", %{conn: conn} do
response =
conn
|> get("/api/pleroma/admin/reports")
|> json_response(:ok)
assert Enum.empty?(response["reports"])
end
test "returns reports", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
response =
conn
|> get("/api/pleroma/admin/reports")
|> json_response(:ok)
[report] = response["reports"]
assert length(response["reports"]) == 1
assert report["id"] == report_id
end
test "returns reports with specified state", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: first_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
{:ok, %{id: second_report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I don't like this user"
})
CommonAPI.update_report_state(second_report_id, "closed")
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "open"
})
|> json_response(:ok)
[open_report] = response["reports"]
assert length(response["reports"]) == 1
assert open_report["id"] == first_report_id
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "closed"
})
|> json_response(:ok)
[closed_report] = response["reports"]
assert length(response["reports"]) == 1
assert closed_report["id"] == second_report_id
response =
conn
|> get("/api/pleroma/admin/reports", %{
"state" => "resolved"
})
|> json_response(:ok)
assert Enum.empty?(response["reports"])
end
test "returns 403 when requested by a non-admin" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
|> get("/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "User is not admin."}
end
test "returns 403 when requested by anonymous" do
conn =
build_conn()
|> get("/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."}
end
end
describe "POST /api/pleroma/admin/reports/:id/respond" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
%{conn: assign(conn, :user, admin)}
end
test "returns created dm", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
response =
conn
|> post("/api/pleroma/admin/reports/#{report_id}/respond", %{
"status" => "I will check it out"
})
|> json_response(:ok)
recipients = Enum.map(response["mentions"], & &1["username"])
assert conn.assigns[:user].nickname in recipients
assert reporter.nickname in recipients
assert response["content"] == "I will check it out"
assert response["visibility"] == "direct"
end
test "returns 400 when status is missing", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/reports/test/respond")
assert json_response(conn, :bad_request) == "Invalid parameters"
end
test "returns 404 when report id is invalid", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/reports/test/respond", %{
"status" => "foo"
})
assert json_response(conn, :not_found) == "Not found"
end
end
describe "PUT /api/pleroma/admin/statuses/:id" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
activity = insert(:note_activity)
%{conn: assign(conn, :user, admin), id: activity.id}
end
test "toggle sensitive flag", %{conn: conn, id: id} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "true"})
|> json_response(:ok)
assert response["sensitive"]
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"sensitive" => "false"})
|> json_response(:ok)
refute response["sensitive"]
end
test "change visibility flag", %{conn: conn, id: id} do
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "public"})
|> json_response(:ok)
assert response["visibility"] == "public"
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "private"})
|> json_response(:ok)
assert response["visibility"] == "private"
response =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "unlisted"})
|> json_response(:ok)
assert response["visibility"] == "unlisted"
end
test "returns 400 when visibility is unknown", %{conn: conn, id: id} do
conn =
conn
|> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"})
assert json_response(conn, :bad_request) == "Unsupported visibility"
end
end
describe "DELETE /api/pleroma/admin/statuses/:id" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
activity = insert(:note_activity)
%{conn: assign(conn, :user, admin), id: activity.id}
end
test "deletes status", %{conn: conn, id: id} do
conn
|> delete("/api/pleroma/admin/statuses/#{id}")
|> json_response(:ok)
refute Activity.get_by_id(id)
end
test "returns error when status is not exist", %{conn: conn} do
conn =
conn
|> delete("/api/pleroma/admin/statuses/test")
assert json_response(conn, :bad_request) == "Could not delete"
end
end
end end

View file

@ -87,6 +87,28 @@ test "it filters out obviously bad tags when accepting a post as Markdown" do
assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')" assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')"
end end
test "it does not allow replies to direct messages that are not direct messages themselves" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"})
assert {:ok, _} =
CommonAPI.post(user, %{
"status" => "suya..",
"visibility" => "direct",
"in_reply_to_status_id" => activity.id
})
Enum.each(["public", "private", "unlisted"], fn visibility ->
assert {:error, {:private_to_public, _}} =
CommonAPI.post(user, %{
"status" => "suya..",
"visibility" => visibility,
"in_reply_to_status_id" => activity.id
})
end)
end
end end
describe "reactions" do describe "reactions" do
@ -239,10 +261,41 @@ test "creates a report" do
data: %{ data: %{
"type" => "Flag", "type" => "Flag",
"content" => ^comment, "content" => ^comment,
"object" => [^target_ap_id, ^activity_ap_id] "object" => [^target_ap_id, ^activity_ap_id],
"state" => "open"
} }
} = flag_activity } = flag_activity
end end
test "updates report state" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
assert report.data["state"] == "resolved"
end
test "does not update report state when state is unsupported" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
end
end end
describe "reblog muting" do describe "reblog muting" do

View file

@ -58,7 +58,7 @@ test "with relays deactivated, it does not publish to the relay", %{
describe "Targets reachability filtering in `publish`" do describe "Targets reachability filtering in `publish`" do
test_with_mock "it federates only to reachable instances via AP", test_with_mock "it federates only to reachable instances via AP",
Federator, Pleroma.Web.ActivityPub.Publisher,
[:passthrough], [:passthrough],
[] do [] do
user = insert(:user) user = insert(:user)
@ -88,13 +88,18 @@ test "with relays deactivated, it does not publish to the relay", %{
{:ok, _activity} = {:ok, _activity} =
CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"}) CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"})
assert called(Federator.publish_single_ap(%{inbox: inbox1, unreachable_since: dt})) assert called(
Pleroma.Web.ActivityPub.Publisher.publish_one(%{
inbox: inbox1,
unreachable_since: dt
})
)
refute called(Federator.publish_single_ap(%{inbox: inbox2})) refute called(Pleroma.Web.ActivityPub.Publisher.publish_one(%{inbox: inbox2}))
end end
test_with_mock "it federates only to reachable instances via Websub", test_with_mock "it federates only to reachable instances via Websub",
Federator, Pleroma.Web.Websub,
[:passthrough], [:passthrough],
[] do [] do
user = insert(:user) user = insert(:user)
@ -122,17 +127,17 @@ test "with relays deactivated, it does not publish to the relay", %{
{:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"}) {:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"})
assert called( assert called(
Federator.publish_single_websub(%{ Pleroma.Web.Websub.publish_one(%{
callback: sub2.callback, callback: sub2.callback,
unreachable_since: dt unreachable_since: dt
}) })
) )
refute called(Federator.publish_single_websub(%{callback: sub1.callback})) refute called(Pleroma.Web.Websub.publish_one(%{callback: sub1.callback}))
end end
test_with_mock "it federates only to reachable instances via Salmon", test_with_mock "it federates only to reachable instances via Salmon",
Federator, Pleroma.Web.Salmon,
[:passthrough], [:passthrough],
[] do [] do
user = insert(:user) user = insert(:user)
@ -162,13 +167,13 @@ test "with relays deactivated, it does not publish to the relay", %{
CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"}) CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"})
assert called( assert called(
Federator.publish_single_salmon(%{ Pleroma.Web.Salmon.publish_one(%{
recipient: remote_user2, recipient: remote_user2,
unreachable_since: dt unreachable_since: dt
}) })
) )
refute called(Federator.publish_single_websub(%{recipient: remote_user1})) refute called(Pleroma.Web.Salmon.publish_one(%{recipient: remote_user1}))
end end
end end

View file

@ -1,194 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# http signatures
# Test data from https://tools.ietf.org/html/draft-cavage-http-signatures-08#appendix-C
defmodule Pleroma.Web.HTTPSignaturesTest do
use Pleroma.DataCase
alias Pleroma.Web.HTTPSignatures
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
@public_key hd(:public_key.pem_decode(File.read!("test/web/http_sigs/pub.key")))
|> :public_key.pem_entry_decode()
@headers %{
"(request-target)" => "post /foo?param=value&pet=dog",
"host" => "example.com",
"date" => "Thu, 05 Jan 2014 21:31:40 GMT",
"content-type" => "application/json",
"digest" => "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=",
"content-length" => "18"
}
@default_signature """
keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w="
"""
@basic_signature """
keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="HUxc9BS3P/kPhSmJo+0pQ4IsCo007vkv6bUm4Qehrx+B1Eo4Mq5/6KylET72ZpMUS80XvjlOPjKzxfeTQj4DiKbAzwJAb4HX3qX6obQTa00/qPDXlMepD2JtTw33yNnm/0xV7fQuvILN/ys+378Ysi082+4xBQFwvhNvSoVsGv4="
"""
@all_headers_signature """
keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0="
"""
test "split up a signature" do
expected = %{
"keyId" => "Test",
"algorithm" => "rsa-sha256",
"signature" =>
"jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=",
"headers" => ["date"]
}
assert HTTPSignatures.split_signature(@default_signature) == expected
end
test "validates the default case" do
signature = HTTPSignatures.split_signature(@default_signature)
assert HTTPSignatures.validate(@headers, signature, @public_key)
end
test "validates the basic case" do
signature = HTTPSignatures.split_signature(@basic_signature)
assert HTTPSignatures.validate(@headers, signature, @public_key)
end
test "validates the all-headers case" do
signature = HTTPSignatures.split_signature(@all_headers_signature)
assert HTTPSignatures.validate(@headers, signature, @public_key)
end
test "it contructs a signing string" do
expected = "date: Thu, 05 Jan 2014 21:31:40 GMT\ncontent-length: 18"
assert expected == HTTPSignatures.build_signing_string(@headers, ["date", "content-length"])
end
test "it validates a conn" do
public_key_pem =
"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGb42rPZIapY4Hfhxrgn\nxKVJczBkfDviCrrYaYjfGxawSw93dWTUlenCVTymJo8meBlFgIQ70ar4rUbzl6GX\nMYvRdku072d1WpglNHXkjKPkXQgngFDrh2sGKtNB/cEtJcAPRO8OiCgPFqRtMiNM\nc8VdPfPdZuHEIZsJ/aUM38EnqHi9YnVDQik2xxDe3wPghOhqjxUM6eLC9jrjI+7i\naIaEygUdyst9qVg8e2FGQlwAeS2Eh8ygCxn+bBlT5OyV59jSzbYfbhtF2qnWHtZy\nkL7KOOwhIfGs7O9SoR2ZVpTEQ4HthNzainIe/6iCR5HGrao/T8dygweXFYRv+k5A\nPQIDAQAB\n-----END PUBLIC KEY-----\n"
[public_key] = :public_key.pem_decode(public_key_pem)
public_key =
public_key
|> :public_key.pem_entry_decode()
conn = %{
req_headers: [
{"host", "localtesting.pleroma.lol"},
{"connection", "close"},
{"content-length", "2316"},
{"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"},
{"date", "Sun, 10 Dec 2017 14:23:49 GMT"},
{"digest", "SHA-256=x/bHADMW8qRrq2NdPb5P9fl0lYpKXXpe5h5maCIL0nM="},
{"content-type", "application/activity+json"},
{"(request-target)", "post /users/demiurge/inbox"},
{"signature",
"keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"i0FQvr51sj9BoWAKydySUAO1RDxZmNY6g7M62IA7VesbRSdFZZj9/fZapLp6YSuvxUF0h80ZcBEq9GzUDY3Chi9lx6yjpUAS2eKb+Am/hY3aswhnAfYd6FmIdEHzsMrpdKIRqO+rpQ2tR05LwiGEHJPGS0p528NvyVxrxMT5H5yZS5RnxY5X2HmTKEgKYYcvujdv7JWvsfH88xeRS7Jlq5aDZkmXvqoR4wFyfgnwJMPLel8P/BUbn8BcXglH/cunR0LUP7sflTxEz+Rv5qg+9yB8zgBsB4C0233WpcJxjeD6Dkq0EcoJObBR56F8dcb7NQtUDu7x6xxzcgSd7dHm5w==\""}
]
}
assert HTTPSignatures.validate_conn(conn, public_key)
end
test "it validates a conn and fetches the key" do
conn = %{
params: %{"actor" => "http://mastodon.example.org/users/admin"},
req_headers: [
{"host", "localtesting.pleroma.lol"},
{"x-forwarded-for", "127.0.0.1"},
{"connection", "close"},
{"content-length", "2307"},
{"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"},
{"date", "Sun, 11 Feb 2018 17:12:01 GMT"},
{"digest", "SHA-256=UXsAnMtR9c7mi1FOf6HRMtPgGI1yi2e9nqB/j4rZ99I="},
{"content-type", "application/activity+json"},
{"signature",
"keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"qXKqpQXUpC3d9bZi2ioEeAqP8nRMD021CzH1h6/w+LRk4Hj31ARJHDwQM+QwHltwaLDUepshMfz2WHSXAoLmzWtvv7xRwY+mRqe+NGk1GhxVZ/LSrO/Vp7rYfDpfdVtkn36LU7/Bzwxvvaa4ZWYltbFsRBL0oUrqsfmJFswNCQIG01BB52BAhGSCORHKtQyzo1IZHdxl8y80pzp/+FOK2SmHkqWkP9QbaU1qTZzckL01+7M5btMW48xs9zurEqC2sM5gdWMQSZyL6isTV5tmkTZrY8gUFPBJQZgihK44v3qgfWojYaOwM8ATpiv7NG8wKN/IX7clDLRMA8xqKRCOKw==\""},
{"(request-target)", "post /users/demiurge/inbox"}
]
}
assert HTTPSignatures.validate_conn(conn)
end
test "validate this" do
conn = %{
params: %{"actor" => "https://niu.moe/users/rye"},
req_headers: [
{"x-forwarded-for", "149.202.73.191"},
{"host", "testing.pleroma.lol"},
{"x-cluster-client-ip", "149.202.73.191"},
{"connection", "upgrade"},
{"content-length", "2396"},
{"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://niu.moe/)"},
{"date", "Sun, 18 Feb 2018 20:31:51 GMT"},
{"digest", "SHA-256=dzH+vLyhxxALoe9RJdMl4hbEV9bGAZnSfddHQzeidTU="},
{"content-type", "application/activity+json"},
{"signature",
"keyId=\"https://niu.moe/users/rye#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"wtxDg4kIpW7nsnUcVJhBk6SgJeDZOocr8yjsnpDRqE52lR47SH6X7G16r7L1AUJdlnbfx7oqcvomoIJoHB3ghP6kRnZW6MyTMZ2jPoi3g0iC5RDqv6oAmDSO14iw6U+cqZbb3P/odS5LkbThF0UNXcfenVNfsKosIJycFjhNQc54IPCDXYq/7SArEKJp8XwEgzmiC2MdxlkVIUSTQYfjM4EG533cwlZocw1mw72e5mm/owTa80BUZAr0OOuhoWARJV9btMb02ZyAF6SCSoGPTA37wHyfM1Dk88NHf7Z0Aov/Fl65dpRM+XyoxdkpkrhDfH9qAx4iuV2VEWddQDiXHA==\""},
{"(request-target)", "post /inbox"}
]
}
assert HTTPSignatures.validate_conn(conn)
end
test "validate this too" do
conn = %{
params: %{"actor" => "https://niu.moe/users/rye"},
req_headers: [
{"x-forwarded-for", "149.202.73.191"},
{"host", "testing.pleroma.lol"},
{"x-cluster-client-ip", "149.202.73.191"},
{"connection", "upgrade"},
{"content-length", "2342"},
{"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://niu.moe/)"},
{"date", "Sun, 18 Feb 2018 21:44:46 GMT"},
{"digest", "SHA-256=vS8uDOJlyAu78cF3k5EzrvaU9iilHCX3chP37gs5sS8="},
{"content-type", "application/activity+json"},
{"signature",
"keyId=\"https://niu.moe/users/rye#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"IN6fHD8pLiDEf35dOaRHzJKc1wBYh3/Yq0ItaNGxUSbJTd2xMjigZbcsVKzvgYYjglDDN+disGNeD+OBKwMqkXWaWe/lyMc9wHvCH5NMhpn/A7qGLY8yToSt4vh8ytSkZKO6B97yC+Nvy6Fz/yMbvKtFycIvSXCq417cMmY6f/aG+rtMUlTbKO5gXzC7SUgGJCtBPCh1xZzu5/w0pdqdjO46ePNeR6JyJSLLV4hfo3+p2n7SRraxM4ePVCUZqhwS9LPt3Zdhy3ut+IXCZgMVIZggQFM+zXLtcXY5HgFCsFQr5WQDu+YkhWciNWtKFnWfAsnsg5sC330lZ/0Z8Z91yA==\""},
{"(request-target)", "post /inbox"}
]
}
assert HTTPSignatures.validate_conn(conn)
end
test "it generates a signature" do
user = insert(:user)
assert HTTPSignatures.sign(user, %{host: "mastodon.example.org"}) =~ "keyId=\""
end
test "this too" do
conn = %{
params: %{"actor" => "https://mst3k.interlinked.me/users/luciferMysticus"},
req_headers: [
{"host", "soc.canned-death.us"},
{"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://mst3k.interlinked.me/)"},
{"date", "Sun, 11 Mar 2018 12:19:36 GMT"},
{"digest", "SHA-256=V7Hl6qDK2m8WzNsjzNYSBISi9VoIXLFlyjF/a5o1SOc="},
{"content-type", "application/activity+json"},
{"signature",
"keyId=\"https://mst3k.interlinked.me/users/luciferMysticus#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"CTYdK5a6lYMxzmqjLOpvRRASoxo2Rqib2VrAvbR5HaTn80kiImj15pCpAyx8IZp53s0Fn/y8MjCTzp+absw8kxx0k2sQAXYs2iy6xhdDUe7iGzz+XLAEqLyZIZfecynaU2nb3Z2XnFDjhGjR1vj/JP7wiXpwp6o1dpDZj+KT2vxHtXuB9585V+sOHLwSB1cGDbAgTy0jx/2az2EGIKK2zkw1KJuAZm0DDMSZalp/30P8dl3qz7DV2EHdDNfaVtrs5BfbDOZ7t1hCcASllzAzgVGFl0BsrkzBfRMeUMRucr111ZG+c0BNOEtJYOHSyZsSSdNknElggCJekONYMYk5ZA==\""},
{"x-forwarded-for", "2607:5300:203:2899::31:1337"},
{"x-forwarded-host", "soc.canned-death.us"},
{"x-forwarded-server", "soc.canned-death.us"},
{"connection", "Keep-Alive"},
{"content-length", "2006"},
{"(request-target)", "post /inbox"}
]
}
assert HTTPSignatures.validate_conn(conn)
end
end

View file

@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF
NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F
UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB
AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA
QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK
kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg
f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u
412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc
mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7
kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA
gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW
G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA==
-----END RSA PRIVATE KEY-----

View file

@ -1,6 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6
Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw
oYi+1hqp1fIekaxsyQIDAQAB
-----END PUBLIC KEY-----

View file

@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.FilterView alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus
alias Pleroma.Web.Push alias Pleroma.Web.Push
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
@ -80,6 +81,19 @@ test "the public timeline", %{conn: conn} do
end) end)
end end
test "the public timeline when public is set to false", %{conn: conn} do
public = Pleroma.Config.get([:instance, :public])
Pleroma.Config.put([:instance, :public], false)
on_exit(fn ->
Pleroma.Config.put([:instance, :public], public)
end)
assert conn
|> get("/api/v1/timelines/public", %{"local" => "False"})
|> json_response(403) == %{"error" => "This resource requires authentication."}
end
test "posting a status", %{conn: conn} do test "posting a status", %{conn: conn} do
user = insert(:user) user = insert(:user)
@ -572,6 +586,7 @@ test "creating a filter", %{conn: conn} do
assert response = json_response(conn, 200) assert response = json_response(conn, 200)
assert response["phrase"] == filter.phrase assert response["phrase"] == filter.phrase
assert response["context"] == filter.context assert response["context"] == filter.context
assert response["irreversible"] == false
assert response["id"] != nil assert response["id"] != nil
assert response["id"] != "" assert response["id"] != ""
end end
@ -2114,7 +2129,7 @@ test "returns favorited DM only when user is logged in and he is one of recipien
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites") |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok) |> json_response(:ok)
assert length(anonymous_response) == 0 assert Enum.empty?(anonymous_response)
end end
test "does not return others' favorited DM when user is not one of recipients", %{ test "does not return others' favorited DM when user is not one of recipients", %{
@ -2138,7 +2153,7 @@ test "does not return others' favorited DM when user is not one of recipients",
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites") |> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|> json_response(:ok) |> json_response(:ok)
assert length(response) == 0 assert Enum.empty?(response)
end end
test "paginates favorites using since_id and max_id", %{ test "paginates favorites using since_id and max_id", %{
@ -3215,4 +3230,129 @@ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{c
replied_to_user = User.get_by_ap_id(replied_to.data["actor"]) replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
end end
describe "create account by app" do
setup do
enabled = Pleroma.Config.get([:app_account_creation, :enabled])
max_requests = Pleroma.Config.get([:app_account_creation, :max_requests])
interval = Pleroma.Config.get([:app_account_creation, :interval])
Pleroma.Config.put([:app_account_creation, :enabled], true)
Pleroma.Config.put([:app_account_creation, :max_requests], 5)
Pleroma.Config.put([:app_account_creation, :interval], 1)
on_exit(fn ->
Pleroma.Config.put([:app_account_creation, :enabled], enabled)
Pleroma.Config.put([:app_account_creation, :max_requests], max_requests)
Pleroma.Config.put([:app_account_creation, :interval], interval)
end)
:ok
end
test "Account registration via Application", %{conn: conn} do
conn =
conn
|> post("/api/v1/apps", %{
client_name: "client_name",
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
scopes: "read, write, follow"
})
%{
"client_id" => client_id,
"client_secret" => client_secret,
"id" => _,
"name" => "client_name",
"redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
"vapid_key" => _,
"website" => nil
} = json_response(conn, 200)
conn =
conn
|> post("/oauth/token", %{
grant_type: "client_credentials",
client_id: client_id,
client_secret: client_secret
})
assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
json_response(conn, 200)
assert token
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
assert refresh
assert scope == "read write follow"
conn =
build_conn()
|> put_req_header("authorization", "Bearer " <> token)
|> post("/api/v1/accounts", %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
agreement: true
})
%{
"access_token" => token,
"created_at" => _created_at,
"scope" => _scope,
"token_type" => "Bearer"
} = json_response(conn, 200)
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
token_from_db = Repo.preload(token_from_db, :user)
assert token_from_db.user
assert token_from_db.user.info.confirmation_pending
end
test "rate limit", %{conn: conn} do
app_token = insert(:oauth_token, user: nil)
conn =
put_req_header(conn, "authorization", "Bearer " <> app_token.token)
|> Map.put(:remote_ip, {15, 15, 15, 15})
for i <- 1..5 do
conn =
conn
|> post("/api/v1/accounts", %{
username: "#{i}lain",
email: "#{i}lain@example.org",
password: "PlzDontHackLain",
agreement: true
})
%{
"access_token" => token,
"created_at" => _created_at,
"scope" => _scope,
"token_type" => "Bearer"
} = json_response(conn, 200)
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
token_from_db = Repo.preload(token_from_db, :user)
assert token_from_db.user
assert token_from_db.user.info.confirmation_pending
end
conn =
conn
|> post("/api/v1/accounts", %{
username: "6lain",
email: "6lain@example.org",
password: "PlzDontHackLain",
agreement: true
})
assert json_response(conn, 403) == %{"error" => "Rate limit exceeded."}
end
end
end end

View file

@ -614,6 +614,27 @@ test "issues a token for request with HTTP basic auth client credentials" do
assert token.scopes == ["scope1", "scope2"] assert token.scopes == ["scope1", "scope2"]
end end
test "issue a token for client_credentials grant type" do
app = insert(:oauth_app, scopes: ["read", "write"])
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "client_credentials",
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
json_response(conn, 200)
assert token
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
assert refresh
assert scope == "read write"
end
test "rejects token exchange with invalid client credentials" do test "rejects token exchange with invalid client credentials" do
user = insert(:user) user = insert(:user)
app = insert(:oauth_app) app = insert(:oauth_app)
@ -644,7 +665,7 @@ test "rejects token exchange for valid credentials belonging to unconfirmed user
password = "testpassword" password = "testpassword"
user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
info_change = Pleroma.User.Info.confirmation_changeset(user.info, :unconfirmed) info_change = Pleroma.User.Info.confirmation_changeset(user.info, need_confirmation: true)
{:ok, user} = {:ok, user} =
user user

View file

@ -0,0 +1,53 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.UtilsTest do
use Pleroma.DataCase
alias Pleroma.Web.OAuth.Token.Utils
import Pleroma.Factory
describe "fetch_app/1" do
test "returns error when credentials is invalid" do
assert {:error, :not_found} =
Utils.fetch_app(%Plug.Conn{params: %{"client_id" => 1, "client_secret" => "x"}})
end
test "returns App by params credentails" do
app = insert(:oauth_app)
assert {:ok, load_app} =
Utils.fetch_app(%Plug.Conn{
params: %{"client_id" => app.client_id, "client_secret" => app.client_secret}
})
assert load_app == app
end
test "returns App by header credentails" do
app = insert(:oauth_app)
header = "Basic " <> Base.encode64("#{app.client_id}:#{app.client_secret}")
conn =
%Plug.Conn{}
|> Plug.Conn.put_req_header("authorization", header)
assert {:ok, load_app} = Utils.fetch_app(conn)
assert load_app == app
end
end
describe "format_created_at/1" do
test "returns formatted created at" do
token = insert(:oauth_token)
date = Utils.format_created_at(token)
token_date =
token.inserted_at
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_unix()
assert token_date == date
end
end
end

View file

@ -7,7 +7,9 @@ defmodule Pleroma.Web.Salmon.SalmonTest do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Salmon alias Pleroma.Web.Salmon
import Mock
import Pleroma.Factory import Pleroma.Factory
@magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" @magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB"
@ -77,7 +79,10 @@ test "it gets a magic key" do
"RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB" "RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB"
end end
test "it pushes an activity to remote accounts it's addressed to" do test_with_mock "it pushes an activity to remote accounts it's addressed to",
Publisher,
[:passthrough],
[] do
user_data = %{ user_data = %{
info: %{ info: %{
salmon: "http://test-example.org/salmon" salmon: "http://test-example.org/salmon"
@ -102,10 +107,8 @@ test "it pushes an activity to remote accounts it's addressed to" do
user = User.get_cached_by_ap_id(activity.data["actor"]) user = User.get_cached_by_ap_id(activity.data["actor"])
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
poster = fn url, _data, _headers -> Salmon.publish(user, activity)
assert url == "http://test-example.org/salmon"
end
Salmon.publish(user, activity, poster) assert called(Publisher.enqueue_one(Salmon, %{recipient: mentioned_user}))
end end
end end

View file

@ -1094,7 +1094,7 @@ test "it returns 500 when user is not local", %{conn: conn, user: user} do
describe "GET /api/account/confirm_email/:id/:token" do describe "GET /api/account/confirm_email/:id/:token" do
setup do setup do
user = insert(:user) user = insert(:user)
info_change = User.Info.confirmation_changeset(user.info, :unconfirmed) info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true)
{:ok, user} = {:ok, user} =
user user
@ -1145,7 +1145,7 @@ test "it returns 500 if token is invalid", %{conn: conn, user: user} do
end end
user = insert(:user) user = insert(:user)
info_change = User.Info.confirmation_changeset(user.info, :unconfirmed) info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true)
{:ok, user} = {:ok, user} =
user user

View file

@ -141,7 +141,7 @@ test "returns the state of safe_dm_mentions flag", %{conn: conn} do
test "it returns the managed config", %{conn: conn} do test "it returns the managed config", %{conn: conn} do
Pleroma.Config.put([:instance, :managed_config], false) Pleroma.Config.put([:instance, :managed_config], false)
Pleroma.Config.put([:fe], theme: "rei-ayanami-towel") Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
response = response =
conn conn
@ -157,29 +157,7 @@ test "it returns the managed config", %{conn: conn} do
|> get("/api/statusnet/config.json") |> get("/api/statusnet/config.json")
|> json_response(:ok) |> json_response(:ok)
assert response["site"]["pleromafe"] assert response["site"]["pleromafe"] == %{"theme" => "asuka-hospital"}
end
test "if :pleroma, :fe is false, it returns the new style config settings", %{conn: conn} do
Pleroma.Config.put([:instance, :managed_config], true)
Pleroma.Config.put([:fe, :theme], "rei-ayanami-towel")
Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
response =
conn
|> get("/api/statusnet/config.json")
|> json_response(:ok)
assert response["site"]["pleromafe"]["theme"] == "rei-ayanami-towel"
Pleroma.Config.put([:fe], false)
response =
conn
|> get("/api/statusnet/config.json")
|> json_response(:ok)
assert response["site"]["pleromafe"]["theme"] == "asuka-hospital"
end end
end end
@ -251,4 +229,22 @@ test "GET /api/pleroma/healthcheck", %{conn: conn} do
assert conn.status in [200, 503] assert conn.status in [200, 503]
end end
describe "POST /api/pleroma/disable_account" do
test "it returns HTTP 200", %{conn: conn} do
user = insert(:user)
response =
conn
|> assign(:user, user)
|> post("/api/pleroma/disable_account", %{"password" => "test"})
|> json_response(:ok)
assert response == %{"status" => "success"}
user = User.get_cached_by_id(user.id)
assert user.info.deactivated == true
end
end
end end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ErrorViewTest do defmodule Pleroma.Web.ErrorViewTest do
use Pleroma.Web.ConnCase, async: true use Pleroma.Web.ConnCase, async: true
import ExUnit.CaptureLog
# Bring render/3 and render_to_string/3 for testing custom views # Bring render/3 and render_to_string/3 for testing custom views
import Phoenix.View import Phoenix.View
@ -13,17 +14,23 @@ test "renders 404.json" do
end end
test "render 500.json" do test "render 500.json" do
assert render(Pleroma.Web.ErrorView, "500.json", []) == assert capture_log(fn ->
%{errors: %{detail: "Internal server error", reason: "nil"}} assert render(Pleroma.Web.ErrorView, "500.json", []) ==
%{errors: %{detail: "Internal server error", reason: "nil"}}
end) =~ "[error] Internal server error: nil"
end end
test "render any other" do test "render any other" do
assert render(Pleroma.Web.ErrorView, "505.json", []) == assert capture_log(fn ->
%{errors: %{detail: "Internal server error", reason: "nil"}} assert render(Pleroma.Web.ErrorView, "505.json", []) ==
%{errors: %{detail: "Internal server error", reason: "nil"}}
end) =~ "[error] Internal server error: nil"
end end
test "render 500.json with reason" do test "render 500.json with reason" do
assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") == assert capture_log(fn ->
%{errors: %{detail: "Internal server error", reason: "\"test reason\""}} assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") ==
%{errors: %{detail: "Internal server error", reason: "\"test reason\""}}
end) =~ "[error] Internal server error: \"test reason\""
end end
end end