diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index dc99b81ee..f9745122a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -52,6 +52,7 @@ unit-testing:
- mix ecto.create
- mix ecto.migrate
- mix test --trace --preload-modules
+ - mix coveralls
lint:
stage: test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0a761c4a..12c439135 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,24 +10,32 @@ 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.
- [Prometheus](https://prometheus.io/) metrics
- 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 update_users_following_followers_counts`
+- Mix Tasks: `mix pleroma.user toggle_confirmed`
- Federation: Support for reports
- Configuration: `safe_dm_mentions` option
- Configuration: `link_name` option
- Configuration: `fetch_initial_posts` option
- Configuration: `notify_email` option
- Configuration: Media proxy `whitelist` option
+- Configuration: `report_uri` option
- Pleroma API: User subscriptions
- Pleroma API: Healthcheck endpoint
- Admin API: Endpoints for listing/revoking invite tokens
- Admin API: Endpoints for making users follow/unfollow each other
- 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: `/api/v1/notifications/destroy_multiple` (glitch-soc extension)
- Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
- Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
+- Mastodon API: `POST /api/v1/accounts` (account creation API)
- ActivityPub C2S: OAuth endpoints
-- Metadata RelMe provider
+- Metadata: RelMe provider
- OAuth: added support for refresh tokens
- Emoji packs and emoji pack manager
@@ -42,8 +50,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Federation: Removed `inReplyToStatusId` from objects
- 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.
-- 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.
+- 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: Add `languages` and `registrations` to `/api/v1/instance`
- Mastodon API: Provide plaintext versions of cw/content in the Status entity
@@ -57,17 +66,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: Actual reblog hiding instead of a dummy
- 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 Ecto to 3.0.7
- 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
### Fixed
- 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
- 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
-- 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
- NodeInfo: Include admins in `staffAccounts`
- ActivityPub: Crashing when requesting empty local user's outbox
@@ -91,6 +101,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: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON
- 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
### Security
diff --git a/COPYING b/COPYING
index eceb68efe..0aede0fba 100644
--- a/COPYING
+++ b/COPYING
@@ -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
, and are distributed under the Creative Commons
Attribution-ShareAlike 4.0 International license, you should have received
diff --git a/README.md b/README.md
index 987f973ea..928f75dc7 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ For clients it supports both the [GNU Social API with Qvitter extensions](https:
- [Client Applications for Pleroma](https://docs-develop.pleroma.social/clients.html)
-No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at .
+If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at .
## Installation
diff --git a/config/config.exs b/config/config.exs
index 42e4cb4ce..e82f08e07 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -48,7 +48,8 @@
config :pleroma, Pleroma.Repo,
types: Pleroma.PostgresTypes,
- telemetry_event: [Pleroma.Repo.Instrumenter]
+ telemetry_event: [Pleroma.Repo.Instrumenter],
+ migration_lock: nil
config :pleroma, Pleroma.Captcha,
enabled: false,
@@ -212,6 +213,11 @@
registrations_open: true,
federating: true,
federation_reachability_timeout_days: 7,
+ federation_publisher_modules: [
+ Pleroma.Web.ActivityPub.Publisher,
+ Pleroma.Web.Websub,
+ Pleroma.Web.Salmon
+ ],
allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true,
@@ -234,6 +240,8 @@
safe_dm_mentions: false,
healthcheck: false
+config :pleroma, :app_account_creation, enabled: true, max_requests: 25, interval: 1800
+
config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because
# of custom emoji. Issue #275 discusses defanging that somehow.
@@ -246,25 +254,6 @@
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,
pleroma_fe: %{
theme: "pleroma-dark",
@@ -478,6 +467,9 @@
config :pleroma, :database, rum_enabled: false
+config :http_signatures,
+ adapter: Pleroma.Signature
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
diff --git a/config/test.exs b/config/test.exs
index f93bc5994..40db66170 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -59,6 +59,10 @@
total_user_limit: 3,
enabled: false
+config :pleroma, :app_account_creation, max_requests: 5
+
+config :pleroma, :http_security, report_uri: "https://endpoint.com"
+
try do
import_config "test.secret.exs"
rescue
diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md
index 2657d1243..b45c5e285 100644
--- a/docs/api/admin_api.md
+++ b/docs/api/admin_api.md
@@ -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`
- Response:
-```JSON
+```json
{
"page_size": integer,
"count": integer,
@@ -45,7 +45,7 @@ Authentication is required and the user must be an admin.
}
```
-## `/api/pleroma/admin/user`
+## `/api/pleroma/admin/users`
### Remove a user
@@ -63,7 +63,7 @@ Authentication is required and the user must be an admin.
- `password`
- Response: User’s nickname
-## `/api/pleroma/admin/user/follow`
+## `/api/pleroma/admin/users/follow`
### Make a user follow another user
- Methods: `POST`
@@ -73,7 +73,7 @@ Authentication is required and the user must be an admin.
- Response:
- "ok"
-## `/api/pleroma/admin/user/unfollow`
+## `/api/pleroma/admin/users/unfollow`
### Make a user unfollow another user
- Methods: `POST`
@@ -92,7 +92,7 @@ Authentication is required and the user must be an admin.
- `nickname`
- Response: User’s object
-```JSON
+```json
{
"deactivated": bool,
"id": integer,
@@ -106,17 +106,17 @@ Authentication is required and the user must be an admin.
- Method: `PUT`
- Params:
- - `nickname`
- - `tags`
+ - `nicknames` (array)
+ - `tags` (array)
### Untag a list of users
- Method: `DELETE`
- Params:
- - `nickname`
- - `tags`
+ - `nicknames` (array)
+ - `tags` (array)
-## `/api/pleroma/admin/permission_group/:nickname`
+## `/api/pleroma/admin/users/:nickname/permission_group`
### Get user user permission groups membership
@@ -124,14 +124,14 @@ Authentication is required and the user must be an admin.
- Params: none
- Response:
-```JSON
+```json
{
"is_moderator": bool,
"is_admin": bool
}
```
-## `/api/pleroma/admin/permission_group/:nickname/:permission_group`
+## `/api/pleroma/admin/users/:nickname/permission_group/:permission_group`
Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesn’t exist.
@@ -141,7 +141,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Params: none
- Response:
-```JSON
+```json
{
"is_moderator": bool,
"is_admin": bool
@@ -165,7 +165,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- On success: JSON of the `user.info`
- Note: An admin cannot revoke their own admin status.
-## `/api/pleroma/admin/activation_status/:nickname`
+## `/api/pleroma/admin/users/:nickname/activation_status`
### Active or deactivate a user
@@ -203,7 +203,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Response:
- On success: URL of the unfollowed relay
-## `/api/pleroma/admin/invite_token`
+## `/api/pleroma/admin/users/invite_token`
### Get an account registration invite token
@@ -215,7 +215,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
]
- Response: invite token (base64 string)
-## `/api/pleroma/admin/invites`
+## `/api/pleroma/admin/users/invites`
### Get a list of generated invites
@@ -223,7 +223,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Params: none
- Response:
-```JSON
+```json
{
"invites": [
@@ -241,7 +241,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
}
```
-## `/api/pleroma/admin/revoke_invite`
+## `/api/pleroma/admin/users/revoke_invite`
### Revoke invite by token
@@ -250,7 +250,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `token`
- Response:
-```JSON
+```json
{
"id": integer,
"token": string,
@@ -264,7 +264,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
```
-## `/api/pleroma/admin/email_invite`
+## `/api/pleroma/admin/users/email_invite`
### Sends registration invite via email
@@ -273,10 +273,287 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `email`
- `name`, optional
-## `/api/pleroma/admin/password_reset`
+## `/api/pleroma/admin/users/:nickname/password_reset`
### Get a password reset token for a given nickname
- Methods: `GET`
- Params: none
- 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": "@lain click on my link https://www.google.com/",
+ "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 `{}`
diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md
index d3ba41b6a..36b47608e 100644
--- a/docs/api/differences_in_mastoapi_responses.md
+++ b/docs/api/differences_in_mastoapi_responses.md
@@ -87,3 +87,13 @@ Additional parameters can be added to the JSON body/Form data:
`POST /oauth/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.
diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md
index 190846de9..dd0b6ca73 100644
--- a/docs/api/pleroma_api.md
+++ b/docs/api/pleroma_api.md
@@ -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
* 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`
### Register a new user
* Method `POST`
diff --git a/docs/config.md b/docs/config.md
index 99cee25cd..450d73fda 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -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`)
* `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
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack
@@ -280,7 +286,8 @@ This will make Pleroma listen on `127.0.0.1` port `8080` and generate urls start
* ``sts``: Whether to additionally send a `Strict-Transport-Security` header
* ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent
* ``ct_max_age``: The maximum age for the `Expect-CT` header if sent
-* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`.
+* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`
+* ``report_uri``: Adds the specified url to `report-uri` and `report-to` group in CSP header.
## :mrf_user_allowlist
diff --git a/docs/installation/debian_based_en.md b/docs/installation/debian_based_en.md
index 9613a329b..9c0ef92d4 100644
--- a/docs/installation/debian_based_en.md
+++ b/docs/installation/debian_based_en.md
@@ -12,6 +12,7 @@ This guide will assume you are on Debian Stretch. This guide should also work wi
* `erlang-tools`
* `erlang-parsetools`
* `erlang-eldap`, if you want to enable ldap authenticator
+* `erlang-ssh`
* `erlang-xmerl`
* `git`
* `build-essential`
@@ -49,7 +50,7 @@ sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
```shell
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
diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md
index ac5dcaaee..41cce6792 100644
--- a/docs/installation/debian_based_jp.md
+++ b/docs/installation/debian_based_jp.md
@@ -14,6 +14,7 @@
- erlang-dev
- erlang-tools
- erlang-parsetools
+- erlang-ssh
- erlang-xmerl (Jessieではバックポートからインストールすること!)
- git
- build-essential
@@ -44,7 +45,7 @@ wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
* 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 (バックエンド) をインストールします
diff --git a/docs/introduction.md b/docs/introduction.md
index 4af0747fe..045dc7c05 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -1,30 +1,30 @@
# Introduction to Pleroma
## What is Pleroma?
-Pleroma is a federated social networking platform, compatible with GNU social, Mastodon and other OStatus and ActivityPub implementations. It is free software licensed under the AGPLv3.
-It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing.
-It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other.
+Pleroma is a federated social networking platform, compatible with GNU social, Mastodon and other OStatus and ActivityPub implementations. It is free software licensed under the AGPLv3.
+It actually consists of two components: a backend, named simply Pleroma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing.
+It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other.
One account on a instance is enough to talk to the entire fediverse!
-
+
## How can I use it?
-Pleroma instances are already widely deployed, a list can be found here:
+Pleroma instances are already widely deployed, a list can be found here:
http://distsn.org/pleroma-instances.html
-If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too!
-Installation instructions can be found here:
+If you don't feel like joining an existing instance, but instead prefer to deploy your own instance, that's easy too!
+Installation instructions can be found here:
[main Pleroma wiki](/)
-
+
## I got an account, now what?
-Great! Now you can explore the fediverse!
-- Open the login page for your Pleroma instance (for ex. https://pleroma.soykaf.com) and login with your username and password.
-(If you don't have one yet, click on Register) :slightly_smiling_face:
+Great! Now you can explore the fediverse!
+- Open the login page for your Pleroma instance (for ex. https://pleroma.soykaf.com) and login with your username and password.
+(If you don't have one yet, click on Register) :slightly_smiling_face:
At this point you will have two columns in front of you.
### Left column
-- first block: here you can see your avatar, your nickname a bio, and statistics (Statuses, Following, Followers).
-Under that you have a text form which allows you to post new statuses. The icon on the left is for uploading media files and attach them to your post. The number under the text form is a character counter, every instance can have a different character limit (the default is 5000).
-If you want to mention someone, type @ + name of the person. A drop-down menu will help you in finding the right person. :slight_smile:
+- first block: here you can see your avatar, your nickname a bio, and statistics (Statuses, Following, Followers).
+Under that you have a text form which allows you to post new statuses. The icon on the left is for uploading media files and attach them to your post. The number under the text form is a character counter, every instance can have a different character limit (the default is 5000).
+If you want to mention someone, type @ + name of the person. A drop-down menu will help you in finding the right person. :slight_smile:
To post your status, simply press Submit.
- second block: Here you can switch between the different timelines:
@@ -38,7 +38,7 @@ To post your status, simply press Submit.
- fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses.
### Right column
-This is where the interesting stuff happens! :slight_smile:
+This is where the interesting stuff happens! :slight_smile:
Depending on the timeline you will see different statuses, but each status has a standard structure:
- Icon + name + link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the replied-to status).
- A + button on the right allows you to Expand/Collapse an entire discussion thread. It also updates in realtime!
@@ -47,9 +47,9 @@ Depending on the timeline you will see different statuses, but each status has a
- Four buttons (left to right): Reply, Repeat, Favorite, Delete.
## Mastodon interface
-If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! :smile:
-Just add a "/web" after your instance url (for ex. https://pleroma.soycaf.com/web) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! :fireworks:
-For more information on the Mastodon interface, please look here:
+If the Pleroma interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! :smile:
+Just add a "/web" after your instance url (for ex. https://pleroma.soycaf.com/web) and you'll end on the Mastodon web interface, but with a Pleroma backend! MAGIC! :fireworks:
+For more information on the Mastodon interface, please look here:
https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md
Remember, what you see is only the frontend part of Mastodon, the backend is still Pleroma.
diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex
index ab9a3a7ff..f650b447d 100644
--- a/lib/mix/tasks/pleroma/database.ex
+++ b/lib/mix/tasks/pleroma/database.ex
@@ -4,6 +4,9 @@
defmodule Mix.Tasks.Pleroma.Database do
alias Mix.Tasks.Pleroma.Common
+ alias Pleroma.Conversation
+ alias Pleroma.Repo
+ alias Pleroma.User
require Logger
use Mix.Task
@@ -19,6 +22,14 @@ defmodule Mix.Tasks.Pleroma.Database do
Options:
- `--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
+
+ ## Remove duplicated items from following and update followers count for all users
+
+ mix pleroma.database update_users_following_followers_counts
"""
def run(["remove_embedded_objects" | args]) do
{options, [], []} =
@@ -32,7 +43,7 @@ def run(["remove_embedded_objects" | args]) do
Common.start_pleroma()
Logger.info("Removing embedded objects")
- Pleroma.Repo.query!(
+ Repo.query!(
"update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
[],
timeout: :infinity
@@ -41,11 +52,24 @@ def run(["remove_embedded_objects" | args]) do
if Keyword.get(options, :vacuum) do
Logger.info("Runnning VACUUM FULL")
- Pleroma.Repo.query!(
+ Repo.query!(
"vacuum full;",
[],
timeout: :infinity
)
end
end
+
+ def run(["bump_all_conversations"]) do
+ Common.start_pleroma()
+ Conversation.bump_for_all_activities()
+ end
+
+ def run(["update_users_following_followers_counts"]) do
+ Common.start_pleroma()
+
+ users = Repo.all(User)
+ Enum.each(users, &User.remove_duplicated_following/1)
+ Enum.each(users, &User.update_follower_count/1)
+ end
end
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index 5cb54c3ca..d2ddf450a 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -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]))
@@ -239,7 +239,7 @@ def run(["gen-pack", src]) do
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("""
@@ -248,11 +248,11 @@ def run(["gen-pack", src]) 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!(
"index.json",
- Poison.encode!(
+ Jason.encode!(
Map.merge(
existing_data,
pack_json
@@ -263,14 +263,14 @@ def run(["gen-pack", src]) do
IO.puts("index.json file has been update with the #{name} pack")
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")
end
end
defp fetch_manifest(from) do
- Poison.decode!(
+ Jason.decode!(
if String.starts_with?(from, "http") do
Tesla.get!(client(), from).body
else
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index 6a83a8c0d..25fc40ea7 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -77,6 +77,10 @@ defmodule Mix.Tasks.Pleroma.User do
## Delete tags from a user.
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
{options, [], []} =
@@ -138,7 +142,7 @@ def run(["new", nickname, email | rest]) do
bio: bio
}
- changeset = User.register_changeset(%User{}, params, confirmed: true)
+ changeset = User.register_changeset(%User{}, params, need_confirmation: false)
{:ok, _user} = User.register(changeset)
Mix.shell().info("User #{nickname} created")
@@ -388,6 +392,21 @@ def run(["delete_activities", nickname]) do
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
info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value})
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index c121e800f..4e54b15ba 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -60,21 +60,24 @@ defmodule Pleroma.Activity do
timestamps()
end
- def with_preloaded_object(query) do
- query
- |> join(
- :inner,
- [activity],
- o in Object,
+ def with_joined_object(query) do
+ join(query, :inner, [activity], o in Object,
on:
fragment(
"(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
o.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
def with_preloaded_bookmark(query, %User{} = user) do
@@ -108,7 +111,7 @@ def get_bookmark(_, _), do: nil
def change(struct, params \\ %{}) do
struct
- |> cast(params, [:data])
+ |> cast(params, [:data, :recipients])
|> validate_required([:data])
|> unique_constraint(:ap_id, name: :activities_unique_apid_index)
end
@@ -132,7 +135,10 @@ def get_by_ap_id_with_object(ap_id) do
end
def get_by_id(id) do
- Repo.get(Activity, id)
+ Activity
+ |> where([a], a.id == ^id)
+ |> restrict_deactivated_users()
+ |> Repo.one()
end
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
create_by_object_ap_id(ap_id)
+ |> restrict_deactivated_users()
|> Repo.one()
end
@@ -314,4 +321,14 @@ def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
def query_by_actor(actor) do
from(a in Activity, where: a.actor == ^actor)
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
diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex
index 106fe5d18..f34be961f 100644
--- a/lib/pleroma/bbs/handler.ex
+++ b/lib/pleroma/bbs/handler.ex
@@ -95,7 +95,6 @@ def handle_command(state, "home") do
activities =
[user.ap_id | user.following]
|> ActivityPub.fetch_activities(params)
- |> ActivityPub.contain_timeline(user)
Enum.each(activities, fn activity ->
puts_activity(activity)
diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex
index 61688e778..18931d5a0 100644
--- a/lib/pleroma/captcha/kocaptcha.ex
+++ b/lib/pleroma/captcha/kocaptcha.ex
@@ -15,7 +15,7 @@ def new do
%{error: "Kocaptcha service unavailable"}
{:ok, res} ->
- json_resp = Poison.decode!(res.body)
+ json_resp = Jason.decode!(res.body)
%{
type: :kocaptcha,
diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex
index 189faa15f..71a47b9fb 100644
--- a/lib/pleroma/config.ex
+++ b/lib/pleroma/config.ex
@@ -12,8 +12,12 @@ def get(key), do: get(key, nil)
def get([key], default), do: get(key, default)
def get([parent_key | keys], default) do
- Application.get_env(:pleroma, parent_key)
- |> get_in(keys) || default
+ case :pleroma
+ |> Application.get_env(parent_key)
+ |> get_in(keys) do
+ nil -> default
+ any -> any
+ end
end
def get(key, default) do
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
index 0345ac19c..240fb1c37 100644
--- a/lib/pleroma/config/deprecation_warnings.ex
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -5,15 +5,6 @@
defmodule Pleroma.Config.DeprecationWarnings do
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
if Pleroma.Config.get([:mrf_hellthread, :threshold]) do
Logger.warn("""
@@ -24,7 +15,6 @@ def check_hellthread_threshold do
end
def warn do
- check_frontend_config_mechanism()
check_hellthread_threshold()
end
end
diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex
index 6e26c5fd4..238c1acf2 100644
--- a/lib/pleroma/conversation.ex
+++ b/lib/pleroma/conversation.ex
@@ -45,10 +45,10 @@ def get_for_ap_id(ap_id) do
2. Create a participation for all the people involved who don't have one already
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),
- object <- Pleroma.Object.normalize(activity),
"Create" <- activity.data["type"],
+ object <- Pleroma.Object.normalize(activity),
"Note" <- object.data["type"],
ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
{:ok, conversation} = create_for_ap_id(ap_id)
@@ -58,7 +58,7 @@ def create_or_bump_for(activity) do
participations =
Enum.map(users, fn user ->
{:ok, participation} =
- Participation.create_for_user_and_conversation(user, conversation)
+ Participation.create_for_user_and_conversation(user, conversation, opts)
participation
end)
@@ -72,4 +72,21 @@ def create_or_bump_for(activity) do
e -> {:error, e}
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
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index 61021fb18..2a11f9069 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -22,15 +22,17 @@ defmodule Pleroma.Conversation.Participation do
def creation_cng(struct, params) do
struct
- |> cast(params, [:user_id, :conversation_id])
+ |> cast(params, [:user_id, :conversation_id, :read])
|> validate_required([:user_id, :conversation_id])
end
- def create_for_user_and_conversation(user, conversation) do
+ def create_for_user_and_conversation(user, conversation, opts \\ []) do
+ read = !!opts[:read]
+
%__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(
- on_conflict: [set: [read: false, updated_at: NaiveDateTime.utc_now()]],
+ on_conflict: [set: [read: read, updated_at: NaiveDateTime.utc_now()]],
returning: true,
conflict_target: [:user_id, :conversation_id]
)
diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex
index df0f72f96..d0e254362 100644
--- a/lib/pleroma/emails/admin_email.ex
+++ b/lib/pleroma/emails/admin_email.ex
@@ -29,7 +29,7 @@ def report(to, reporter, account, statuses, comment) do
end
statuses_html =
- if length(statuses) > 0 do
+ if is_list(statuses) && length(statuses) > 0 do
statuses_list_html =
statuses
|> Enum.map(fn
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
index 79efc29f0..90457dadf 100644
--- a/lib/pleroma/filter.ex
+++ b/lib/pleroma/filter.ex
@@ -38,7 +38,8 @@ def get_filters(%User{id: user_id} = _user) do
query =
from(
f in Pleroma.Filter,
- where: f.user_id == ^user_id
+ where: f.user_id == ^user_id,
+ order_by: [desc: :id]
)
Repo.all(query)
diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex
index 1d2e0785c..b3319e137 100644
--- a/lib/pleroma/gopher/server.ex
+++ b/lib/pleroma/gopher/server.ex
@@ -77,13 +77,13 @@ def render_activities(activities) do
user = User.get_cached_by_ap_id(activity.data["actor"])
object = Object.normalize(activity)
- like_count = object["like_count"] || 0
- announcement_count = object["announcement_count"] || 0
+ like_count = object.data["like_count"] || 0
+ announcement_count = object.data["announcement_count"] || 0
link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <>
info("#{like_count} likes, #{announcement_count} repeats") <>
"i\tfake\t(NULL)\t0\r\n" <>
- info(HTML.strip_tags(String.replace(object["content"], "
", "\r")))
+ info(HTML.strip_tags(String.replace(object.data["content"], "
", "\r")))
end)
|> Enum.join("i\tfake\t(NULL)\t0\r\n")
end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index dd274cf6b..844264307 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -33,6 +33,13 @@ def changeset(%Notification{} = notification, attrs) do
def for_user_query(user) do
Notification
|> 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(:left, [n, a], object in Object,
on:
diff --git a/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
new file mode 100644
index 000000000..317fd5445
--- /dev/null
+++ b/lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# 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
diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex
index a476f1d49..485ddfbc7 100644
--- a/lib/pleroma/plugs/http_security_plug.ex
+++ b/lib/pleroma/plugs/http_security_plug.ex
@@ -20,8 +20,9 @@ def call(conn, _options) do
defp headers do
referrer_policy = Config.get([:http_security, :referrer_policy])
+ report_uri = Config.get([:http_security, :report_uri])
- [
+ headers = [
{"x-xss-protection", "1; mode=block"},
{"x-permitted-cross-domain-policies", "none"},
{"x-frame-options", "DENY"},
@@ -30,12 +31,27 @@ defp headers do
{"x-download-options", "noopen"},
{"content-security-policy", csp_string() <> ";"}
]
+
+ if report_uri do
+ report_group = %{
+ "group" => "csp-endpoint",
+ "max-age" => 10_886_400,
+ "endpoints" => [
+ %{"url" => report_uri}
+ ]
+ }
+
+ headers ++ [{"reply-to", Jason.encode!(report_group)}]
+ else
+ headers
+ end
end
defp csp_string do
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
static_url = Pleroma.Web.Endpoint.static_url()
websocket_url = Pleroma.Web.Endpoint.websocket_url()
+ report_uri = Config.get([:http_security, :report_uri])
connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
@@ -53,7 +69,7 @@ defp csp_string do
"script-src 'self'"
end
- [
+ main_part = [
"default-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
@@ -63,11 +79,14 @@ defp csp_string do
"font-src 'self'",
"manifest-src 'self'",
connect_src,
- script_src,
- if scheme == "https" do
- "upgrade-insecure-requests"
- end
+ script_src
]
+
+ report = if report_uri, do: ["report-uri #{report_uri}; report-to csp-endpoint"], else: []
+
+ insecure = if scheme == "https", do: ["upgrade-insecure-requests"], else: []
+
+ (main_part ++ report ++ insecure)
|> Enum.join("; ")
end
diff --git a/lib/pleroma/plugs/http_signature.ex b/lib/pleroma/plugs/http_signature.ex
index 21c195713..e2874c469 100644
--- a/lib/pleroma/plugs/http_signature.ex
+++ b/lib/pleroma/plugs/http_signature.ex
@@ -4,7 +4,6 @@
defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
alias Pleroma.Web.ActivityPub.Utils
- alias Pleroma.Web.HTTPSignatures
import Plug.Conn
require Logger
diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex
index 9d43732eb..86bc4aa3a 100644
--- a/lib/pleroma/plugs/oauth_plug.ex
+++ b/lib/pleroma/plugs/oauth_plug.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Plugs.OAuthPlug do
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token
@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(:user, user)
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
def call(conn, _) do
- with {:ok, token_str} <- fetch_token_str(conn),
- {:ok, user, token_record} <- fetch_user_and_token(token_str) do
- conn
- |> assign(:token, token_record)
- |> assign(:user, user)
- else
- _ -> conn
+ case fetch_token_str(conn) do
+ {:ok, token} ->
+ with {:ok, user, token_record} <- fetch_user_and_token(token) do
+ conn
+ |> assign(:token, token_record)
+ |> assign(:user, user)
+ 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
@@ -54,6 +76,16 @@ defp fetch_user_and_token(token) do
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
#
@spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
diff --git a/lib/pleroma/plugs/rate_limit_plug.ex b/lib/pleroma/plugs/rate_limit_plug.ex
new file mode 100644
index 000000000..466f64a79
--- /dev/null
+++ b/lib/pleroma/plugs/rate_limit_plug.ex
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# 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
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
new file mode 100644
index 000000000..b7ecf00a0
--- /dev/null
+++ b/lib/pleroma/signature.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# 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
diff --git a/lib/pleroma/uploaders/swift/keystone.ex b/lib/pleroma/uploaders/swift/keystone.ex
index 3046cdbd2..dd44c7561 100644
--- a/lib/pleroma/uploaders/swift/keystone.ex
+++ b/lib/pleroma/uploaders/swift/keystone.ex
@@ -14,7 +14,7 @@ def process_url(url) do
def process_response_body(body) do
body
- |> Poison.decode!()
+ |> Jason.decode!()
end
def get_token do
@@ -38,7 +38,7 @@ def get_token do
end
def make_auth_body(username, password, tenant) do
- Poison.encode!(%{
+ Jason.encode!(%{
:auth => %{
:passwordCredentials => %{
:username => username,
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 427400aa1..28da310ee 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -55,7 +55,7 @@ defmodule Pleroma.User do
field(:last_refreshed_at, :naive_datetime_usec)
has_many(:notifications, Notification)
has_many(:registrations, Registration)
- embeds_one(:info, Pleroma.User.Info)
+ embeds_one(:info, User.Info)
timestamps()
end
@@ -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 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,
follower_count: user.info.follower_count,
locked: user.info.locked,
@@ -117,6 +115,20 @@ def user_info(%User{} = user) do
}
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
params =
params
@@ -154,7 +166,7 @@ def remote_user_creation(params) do
def update_changeset(struct, params \\ %{}) do
struct
- |> cast(params, [:bio, :name, :avatar])
+ |> cast(params, [:bio, :name, :avatar, :following])
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: 5000)
@@ -204,14 +216,15 @@ def reset_password(user, data) do
end
def register_changeset(struct, params \\ %{}, opts \\ []) do
- confirmation_status =
- if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
- :confirmed
+ need_confirmation? =
+ if is_nil(opts[:need_confirmation]) do
+ Pleroma.Config.get([:instance, :account_activation_required])
else
- :unconfirmed
+ opts[:need_confirmation]
end
- info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
+ info_change =
+ User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
changeset =
struct
@@ -220,7 +233,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
- |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
+ |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000)
@@ -254,7 +267,7 @@ defp autofollow_users(user) do
candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
autofollowed_users =
- User.Query.build(%{nickname: candidates, local: true})
+ User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|> Repo.all()
follow_all(user, autofollowed_users)
@@ -265,7 +278,7 @@ def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset),
{:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user),
- {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
+ {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
end
@@ -412,24 +425,6 @@ def following?(%User{} = follower, %User{} = followed) do
Enum.member?(follower.following, followed.follower_address)
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
user.info.locked || false
end
@@ -551,8 +546,7 @@ def get_or_fetch_by_nickname(nickname) do
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
- # TODO turn into job
- {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
+ fetch_initial_posts(user)
end
{:ok, user}
@@ -563,19 +557,12 @@ def get_or_fetch_by_nickname(nickname) do
end
@doc "Fetch some posts when the user has just been federated with"
- def fetch_initial_posts(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
- )
- end
+ def fetch_initial_posts(user),
+ do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
@spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_followers_query(%User{} = user, nil) do
- User.Query.build(%{followers: user})
+ User.Query.build(%{followers: user, deactivated: false})
end
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()
def get_friends_query(%User{} = user, nil) do
- User.Query.build(%{friends: user})
+ User.Query.build(%{friends: user, deactivated: false})
end
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)
- cng =
- change(user)
- |> put_embed(:info, info_cng)
-
- update_and_set_cache(cng)
+ user
+ |> change()
+ |> put_embed(:info, info_cng)
+ |> update_and_set_cache()
end
def update_follower_count(%User{} = user) do
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
|> where(id: ^user.id)
@@ -722,9 +709,21 @@ def update_follower_count(%User{} = user) do
end
end
+ def remove_duplicated_following(%User{following: following} = user) do
+ uniq_following = Enum.uniq(following)
+
+ if length(following) == length(uniq_following) do
+ {:ok, user}
+ else
+ user
+ |> update_changeset(%{following: uniq_following})
+ |> update_and_set_cache()
+ end
+ end
+
@spec get_users_from_set([String.t()], boolean()) :: [User.t()]
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
User.Query.build(criteria)
@@ -733,7 +732,7 @@ def get_users_from_set(ap_ids, local_only \\ true) do
@spec get_recipients_from_activity(Activity.t()) :: [User.t()]
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()
end
@@ -831,6 +830,7 @@ defp fts_search_subquery(term, query \\ User) do
^processed_query
)
)
+ |> restrict_deactivated()
end
defp trigram_search_subquery(term) do
@@ -849,23 +849,7 @@ defp trigram_search_subquery(term) do
},
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
)
- end
-
- 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
- )
+ |> restrict_deactivated()
end
def mute(muter, %User{ap_id: ap_id}) do
@@ -998,19 +982,19 @@ def subscribed_to?(user, %{ap_id: ap_id}) do
@spec muted_users(User.t()) :: [User.t()]
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()
end
@spec blocked_users(User.t()) :: [User.t()]
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()
end
@spec subscribers(User.t()) :: [User.t()]
def subscribers(user) do
- User.Query.build(%{ap_id: user.info.subscribers})
+ User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
|> Repo.all()
end
@@ -1038,14 +1022,25 @@ def unblock_domain(user, domain) do
update_and_set_cache(cng)
end
+ def deactivate_async(user, status \\ true) do
+ PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
+ end
+
def deactivate(%User{} = user, status \\ true) do
info_cng = User.Info.set_activation_status(user.info, status)
- cng =
- change(user)
- |> put_embed(:info, info_cng)
+ with {:ok, friends} <- User.get_friends(user),
+ {:ok, followers} <- User.get_followers(user),
+ {: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
def update_notification_settings(%User{} = user, settings \\ %{}) do
@@ -1076,11 +1071,79 @@ def perform(:delete, %User{} = user) do
delete_user_activities(user)
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
stream =
ap_id
|> Activity.query_by_actor()
- |> Activity.with_preloaded_object()
|> Repo.stream()
Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
@@ -1129,8 +1192,8 @@ def get_or_fetch_by_ap_id(ap_id) do
resp = fetch_by_ap_id(ap_id)
if should_fetch_initial do
- with {:ok, %User{} = user} = resp do
- {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
+ with {:ok, %User{} = user} <- resp do
+ fetch_initial_posts(user)
end
end
@@ -1319,11 +1382,24 @@ def error_user(ap_id) do
@spec all_superusers() :: [User.t()]
def all_superusers do
- User.Query.build(%{super_users: true, local: true})
+ User.Query.build(%{super_users: true, local: true, deactivated: false})
|> Repo.all()
end
def showing_reblogs?(%User{} = user, %User{} = target) do
target.ap_id not in user.info.muted_reblogs
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
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index 1b81619ce..5f0cefc00 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -8,6 +8,8 @@ defmodule Pleroma.User.Info do
alias Pleroma.User.Info
+ @type t :: %__MODULE__{}
+
embedded_schema do
field(:banner, :map, default: %{})
field(:background, :map, default: %{})
@@ -210,21 +212,23 @@ def profile_update(info, params) do
])
end
- def confirmation_changeset(info, :confirmed) do
- confirmation_changeset(info, %{
- confirmation_pending: false,
- confirmation_token: nil
- })
- end
+ @spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t()
+ def confirmation_changeset(info, opts) do
+ need_confirmation? = Keyword.get(opts, :need_confirmation)
- def confirmation_changeset(info, :unconfirmed) do
- confirmation_changeset(info, %{
- confirmation_pending: true,
- confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
- })
- end
+ params =
+ if need_confirmation? do
+ %{
+ confirmation_pending: true,
+ confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
+ }
+ else
+ %{
+ confirmation_pending: false,
+ confirmation_token: nil
+ }
+ end
- def confirmation_changeset(info, params) do
cast(info, params, [:confirmation_pending, :confirmation_token])
end
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index 2dfe5ce92..ace9c05f2 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -118,7 +118,11 @@ defp compose_query({:active, _}, query) do
|> where([u], not is_nil(u.nickname))
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([u], not is_nil(u.nickname))
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 8f8c23a9b..5c3156978 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -5,7 +5,6 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Conversation
- alias Pleroma.Instances
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Fetcher
@@ -15,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier
- alias Pleroma.Web.Federator
alias Pleroma.Web.WebFinger
import Ecto.Query
@@ -24,8 +22,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
require Logger
- @httpoison Application.get_env(:pleroma, :httpoison)
-
# For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do
@@ -137,9 +133,7 @@ def insert(map, local \\ true, fake \\ false) when is_map(map) do
activity
end
- Task.start(fn ->
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- end)
+ PleromaJobQueue.enqueue(:background, Pleroma.Web.RichMedia.Helpers, [:fetch, activity])
Notification.create_notifications(activity)
@@ -546,8 +540,6 @@ defp restrict_visibility(query, %{visibility: visibility})
)
)
- Ecto.Adapters.SQL.to_sql(:all, Repo, query)
-
query
else
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)
)
- Ecto.Adapters.SQL.to_sql(:all, Repo, query)
-
query
end
@@ -575,6 +565,18 @@ defp restrict_visibility(_query, %{visibility: visibility})
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
params =
params
@@ -701,6 +703,12 @@ defp restrict_type(query, %{"type" => type}) do
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
from(
activity in query,
@@ -756,8 +764,11 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info.blocks || []
domain_blocks = info.domain_blocks || []
+ query =
+ if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
+
from(
- activity in query,
+ [activity, object: o] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (? && ?)", activity.recipients, ^blocks),
where:
@@ -767,7 +778,8 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
activity.data,
^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
@@ -849,15 +861,18 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_local(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
+ |> restrict_state(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(opts)
|> restrict_muted(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
+ |> restrict_thread_visibility(opts)
|> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
+ |> Activity.restrict_deactivated_users()
end
def fetch_activities(recipients, opts \\ %{}) do
@@ -961,89 +976,6 @@ def make_user_from_nickname(nickname) do
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
def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user)
@@ -1054,11 +986,10 @@ def contain_activity(%Activity{} = activity, %User{} = user) do
contain_broken_threads(activity, user)
end
- # do post-processing on a timeline
- def contain_timeline(timeline, user) do
- timeline
- |> Enum.filter(fn activity ->
- contain_activity(activity, user)
- end)
+ def fetch_direct_messages_query do
+ Activity
+ |> restrict_type(%{"type" => "Create"})
+ |> restrict_visibility(%{visibility: "direct"})
+ |> order_by([activity], asc: activity.id)
end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index 2f105700b..50426e920 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -55,7 +55,7 @@ defp check_media_nsfw(
object =
if Enum.member?(Pleroma.Config.get([:mrf_simple, :media_nsfw]), actor_host) do
tags = (child_object["tag"] || []) ++ ["nsfw"]
- child_object = Map.put(child_object, "tags", tags)
+ child_object = Map.put(child_object, "tag", tags)
child_object = Map.put(child_object, "sensitive", true)
Map.put(object, "object", child_object)
else
diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
index b52be30e7..6683b8d8e 100644
--- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex
@@ -31,7 +31,7 @@ defp process_tag(
object =
object
- |> Map.put("tags", tags)
+ |> Map.put("tag", tags)
|> Map.put("sensitive", true)
message = Map.put(message, "object", object)
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
new file mode 100644
index 000000000..11dba87de
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -0,0 +1,152 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 508f3532f..5edd8ccc7 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -11,7 +11,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Object.Containment
alias Pleroma.Repo
alias Pleroma.User
- alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 581b9d1ab..ca8a0844b 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -20,6 +20,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
require Logger
@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,
# 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,
"content" => params.content,
"object" => object,
- "context" => params.context
+ "context" => params.context,
+ "state" => "open"
}
|> Map.merge(additional)
end
@@ -682,7 +685,7 @@ def make_flag_data(params, additional) do
"""
def fetch_ordered_collection(from, pages_left, acc \\ []) do
with {:ok, response} <- Tesla.get(from),
- {:ok, collection} <- Poison.decode(response.body) do
+ {:ok, collection} <- Jason.decode(response.body) do
case collection["type"] do
"OrderedCollection" ->
# 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
+
+ #### 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
diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex
index 6dee61dd6..93b50ee47 100644
--- a/lib/pleroma/web/activity_pub/visibility.ex
+++ b/lib/pleroma/web/activity_pub/visibility.ex
@@ -1,6 +1,7 @@
defmodule Pleroma.Web.ActivityPub.Visibility do
alias Pleroma.Activity
alias Pleroma.Object
+ alias Pleroma.Repo
alias Pleroma.User
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
@@ -13,11 +14,12 @@ def is_public?(data) do
end
def is_private?(activity) do
- unless is_public?(activity) do
- follower_address = User.get_cached_by_ap_id(activity.data["actor"]).follower_address
- Enum.any?(activity.data["to"], &(&1 == follower_address))
+ with false <- is_public?(activity),
+ %User{follower_address: follower_address} <-
+ User.get_cached_by_ap_id(activity.data["actor"]) do
+ follower_address in activity.data["to"]
else
- false
+ _ -> false
end
end
@@ -38,24 +40,37 @@ def visible_for_user?(activity, user) do
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end
- # guard
- def entire_thread_visible_for_user?(nil, _user), do: false
+ def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
+ {: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
- # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
+ result
+ end
- def entire_thread_visible_for_user?(
- %Activity{} = tail,
- # %Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
- user
- ) 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)
+ def get_visibility(object) do
+ public = "https://www.w3.org/ns/activitystreams#Public"
+ to = object.data["to"] || []
+ cc = object.data["cc"] || []
- _ ->
- visible_for_user?(tail, user)
+ 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
end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index b553d96a8..de2a13c01 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -4,11 +4,16 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller
+ alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.UserInviteToken
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI.AccountView
+ alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.StatusView
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@@ -59,7 +64,7 @@ def user_create(
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)
conn
@@ -287,12 +292,88 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
|> json(token.token)
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
conn
|> put_status(404)
|> json("Not found")
end
+ def errors(conn, {:error, reason}) do
+ conn
+ |> put_status(400)
+ |> json(reason)
+ end
+
def errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
new file mode 100644
index 000000000..47a73dc7e
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# 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
diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex
index dd79cdcf7..c4a6fce08 100644
--- a/lib/pleroma/web/auth/pleroma_authenticator.ex
+++ b/lib/pleroma/web/auth/pleroma_authenticator.ex
@@ -74,7 +74,7 @@ def create_from_registration(
password_confirmation: random_password
},
external: true,
- confirmed: true
+ need_confirmation: false
)
|> Repo.insert(),
{:ok, _} <-
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index b53869c75..a599ffee5 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -71,6 +71,9 @@ def delete(activity_id, user) do
{:ok, _} <- unpin(activity_id, user),
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
+ else
+ _ ->
+ {:error, "Could not delete"}
end
end
@@ -116,32 +119,34 @@ def unfavorite(id_or_ap_id, user) do
end
end
- def get_visibility(%{"visibility" => visibility})
+ def get_visibility(%{"visibility" => visibility}, in_reply_to)
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
- case get_replied_to_activity(status_id) do
- nil ->
- "public"
+ def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do
+ visibility = get_replied_to_visibility(in_reply_to)
+ {visibility, visibility}
+ end
- in_reply_to ->
- # XXX: these heuristics should be moved out of MastodonAPI.
- with %Object{} = object <- Object.normalize(in_reply_to) do
- Pleroma.Web.MastodonAPI.StatusView.get_visibility(object)
- end
+ def get_visibility(_, in_reply_to), do: {"public", get_replied_to_visibility(in_reply_to)}
+
+ def get_replied_to_visibility(nil), do: nil
+
+ def get_replied_to_visibility(activity) do
+ with %Object{} = object <- Object.normalize(activity) do
+ Pleroma.Web.ActivityPub.Visibility.get_visibility(object)
end
end
- def get_visibility(_), do: "public"
-
def post(user, %{"status" => status} = data) do
- visibility = get_visibility(data)
limit = Pleroma.Config.get([:instance, :limit])
with status <- String.trim(status),
attachments <- attachments_from_ids(data),
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} <-
make_content_html(
status,
@@ -185,6 +190,8 @@ def post(user, %{"status" => status} = data) do
)
res
+ else
+ e -> {:error, e}
end
end
@@ -193,7 +200,7 @@ def update(user) do
user =
with emoji <- emoji_from_profile(user),
source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
- info_cng <- Pleroma.User.Info.set_source_data(user.info, source_data),
+ info_cng <- User.Info.set_source_data(user.info, source_data),
change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(change) do
user
@@ -226,7 +233,7 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
true <- Enum.member?(object_to, "https://www.w3.org/ns/activitystreams#Public"),
%{valid?: true} = info_changeset <-
- Pleroma.User.Info.add_pinnned_activity(user.info, activity),
+ User.Info.add_pinnned_activity(user.info, activity),
changeset <-
Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
{:ok, _user} <- User.update_and_set_cache(changeset) do
@@ -243,7 +250,7 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
def unpin(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
%{valid?: true} = info_changeset <-
- Pleroma.User.Info.remove_pinnned_activity(user.info, activity),
+ User.Info.remove_pinnned_activity(user.info, activity),
changeset <-
Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
{:ok, _user} <- User.update_and_set_cache(changeset) do
@@ -311,6 +318,60 @@ def report(user, data) do
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
ap_id = muted.ap_id
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 1dfe50b40..bee2fd159 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -237,13 +237,11 @@ def make_note_data(
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
}
- if in_reply_to do
- in_reply_to_object = Object.normalize(in_reply_to)
-
- object
- |> Map.put("inReplyTo", in_reply_to_object.data["id"])
+ with false <- is_nil(in_reply_to),
+ %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
+ Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
else
- object
+ _ -> object
end
end
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 7f939991d..9ef30e885 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -29,6 +29,13 @@ defmodule Pleroma.Web.Endpoint do
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
)
+ plug(Plug.Static.IndexHtml, at: "/pleroma/admin/")
+
+ plug(Plug.Static,
+ at: "/pleroma/admin/",
+ from: {:pleroma, "priv/static/adminfe/"}
+ )
+
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index 29e178ba9..169fdf4dc 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -7,13 +7,10 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Object.Containment
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
- alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Federator.RetryQueue
- alias Pleroma.Web.OStatus
- alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub
@@ -42,14 +39,6 @@ def publish(activity, priority \\ 1) do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)
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
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])
end
@@ -62,10 +51,6 @@ def refresh_subscriptions do
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
end
- def publish_single_salmon(params) do
- PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])
- end
-
# Job Worker Callbacks
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
{:ok, actor} = WebFinger.ensure_keys_present(actor)
- if Visibility.is_public?(activity) do
- if OStatus.is_representable?(activity) do
- Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end)
- Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
-
- 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)
+ Publisher.publish(actor, activity)
end
end
@@ -148,25 +117,11 @@ def perform(:incoming_ap_doc, params) do
_e ->
# Just drop those for now
Logger.info("Unhandled activity")
- Logger.info(Poison.encode!(params, pretty: 2))
+ Logger.info(Jason.encode!(params, pretty: true))
:error
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(
:publish_single_websub,
%{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params
diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex
new file mode 100644
index 000000000..70f870244
--- /dev/null
+++ b/lib/pleroma/web/federator/publisher.ex
@@ -0,0 +1,95 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# 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(User.t(), Activity.t()) :: :ok | {:error, any()}
+
+ @spec publish(User.t(), 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(User.t()) :: list()
+
+ @spec gather_webfinger_links(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
diff --git a/lib/pleroma/web/http_signatures/http_signatures.ex b/lib/pleroma/web/http_signatures/http_signatures.ex
deleted file mode 100644
index 8e2e2a44b..000000000
--- a/lib/pleroma/web/http_signatures/http_signatures.ex
+++ /dev/null
@@ -1,91 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors
-# 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
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index fc0100b1e..1051861ff 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -39,12 +39,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.ControllerHelper
import Ecto.Query
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)
@local_mastodon_name "Mastodon-Local"
@@ -168,7 +178,7 @@ def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
end
end
- @mastodon_api_level "2.6.5"
+ @mastodon_api_level "2.7.2"
def masto_instance(conn, _params) do
instance = Config.get(:instance)
@@ -293,7 +303,6 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do
activities =
[user.ap_id | user.following]
|> ActivityPub.fetch_activities(params)
- |> ActivityPub.contain_timeline(user)
|> Enum.reverse()
conn
@@ -1236,7 +1245,7 @@ def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_id
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
- %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
+ %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
end
end)
@@ -1559,7 +1568,7 @@ def create_filter(
user_id: user.id,
phrase: phrase,
context: context,
- hide: Map.get(params, "irreversible", nil),
+ hide: Map.get(params, "irreversible", false),
whole_word: Map.get(params, "boolean", true)
# expires_at
}
@@ -1716,6 +1725,53 @@ def reports(%{assigns: %{user: user}} = conn, params) do
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
participations = Participation.for_user_with_last_activity_id(user, params)
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 779b9a382..134c07b7e 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -40,7 +40,7 @@ def render("relationship.json", %{user: %User{} = user, target: %User{} = target
follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)
requested =
- if follow_activity do
+ if follow_activity && !User.following?(target, user) do
follow_activity.data["state"] == "pending"
else
false
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index bd2372944..c93d915e5 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -16,6 +16,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
+ import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]
+
# TODO: Add cached version.
defp get_replied_to_activities(activities) do
activities
@@ -340,30 +342,6 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
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
with name when not is_nil(name) and name != "" <- object.data["name"] do
"#{name}
#{object.data["content"]}"
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index 216a962bd..3bf2a0fbc 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.MRF
+ alias Pleroma.Web.Federator.Publisher
plug(Pleroma.Web.FederatingPlug)
@@ -137,7 +138,7 @@ def raw_nodeinfo do
name: Pleroma.Application.name() |> String.downcase(),
version: Pleroma.Application.version()
},
- protocols: ["ostatus", "activitypub"],
+ protocols: Publisher.gather_nodeinfo_protocol_names(),
services: %{
inbound: [],
outbound: []
diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex
index bccc2ac96..ddcdb1871 100644
--- a/lib/pleroma/web/oauth/app.ex
+++ b/lib/pleroma/web/oauth/app.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.App do
import Ecto.Changeset
@type t :: %__MODULE__{}
+
schema "apps" do
field(:client_name, :string)
field(:redirect_uris, :string)
diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex
index ca3901cc4..18973413e 100644
--- a/lib/pleroma/web/oauth/authorization.ex
+++ b/lib/pleroma/web/oauth/authorization.ex
@@ -14,39 +14,57 @@ defmodule Pleroma.Web.OAuth.Authorization do
import Ecto.Query
@type t :: %__MODULE__{}
+
schema "oauth_authorizations" do
field(:token, :string)
field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime_usec)
field(:used, :boolean, default: false)
- belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
+ belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:app, App)
timestamps()
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
- scopes = scopes || app.scopes
- token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
-
- authorization = %Authorization{
- token: token,
- used: false,
+ %{
+ scopes: scopes || app.scopes,
user_id: user.id,
- app_id: app.id,
- scopes: scopes,
- valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
+ app_id: app.id
}
-
- Repo.insert(authorization)
+ |> create_changeset()
+ |> Repo.insert()
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
auth
|> cast(params, [:used])
|> validate_required([:used])
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
if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do
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"}
+ @spec delete_user_authorizations(User.t()) :: {integer(), any()}
def delete_user_authorizations(%User{id: user_id}) do
from(
a in Pleroma.Web.OAuth.Authorization,
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 8ee0da667..ae2b80d95 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -19,8 +19,6 @@ defmodule Pleroma.Web.OAuth.OAuthController do
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
- @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)
-
plug(:fetch_session)
plug(:fetch_flash)
@@ -144,14 +142,14 @@ defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do
@doc "Renew access_token with refresh_token"
def token_exchange(
conn,
- %{"grant_type" => "refresh_token", "refresh_token" => token} = params
+ %{"grant_type" => "refresh_token", "refresh_token" => token} = _params
) 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, token} <- RefreshToken.grant(token) do
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
_error ->
put_status(conn, 400)
@@ -160,14 +158,14 @@ def token_exchange(
end
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"]),
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do
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
_error ->
put_status(conn, 400)
@@ -179,14 +177,14 @@ def token_exchange(
conn,
%{"grant_type" => "password"} = params
) do
- with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)},
- %App{} = app <- get_app_from_request(conn, params),
+ with {:ok, %User{} = user} <- Authenticator.get_user(conn),
+ {:ok, app} <- Token.Utils.fetch_app(conn),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:user_active, true} <- {:user_active, !user.info.deactivated},
{:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do
- json(conn, response_token(user, token))
+ json(conn, Token.Response.build(user, token))
else
{:auth_active, false} ->
# Per https://github.com/tootsuite/mastodon/blob/
@@ -218,11 +216,23 @@ def token_exchange(
token_exchange(conn, params)
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
def token_exchange(conn, params), do: bad_request(conn, params)
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
json(conn, %{})
else
@@ -252,7 +262,7 @@ def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attr
auth_attrs
|> Map.delete("scopes")
|> Map.put("scope", scope)
- |> Poison.encode!()
+ |> Jason.encode!()
params =
auth_attrs
@@ -316,7 +326,7 @@ def callback(conn, params) do
end
defp callback_params(%{"state" => state} = params) do
- Map.merge(params, Poison.decode!(state))
+ Map.merge(params, Jason.decode!(state))
end
def registration_details(conn, %{"authorization" => auth_attrs}) do
@@ -405,33 +415,6 @@ defp do_create_authorization(
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
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),
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()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
defp validate_scopes(app, params) do
diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex
index 4e5d1d118..66c95c2e9 100644
--- a/lib/pleroma/web/oauth/token.ex
+++ b/lib/pleroma/web/oauth/token.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.OAuth.Token do
field(:refresh_token, :string)
field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime_usec)
- belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
+ belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:app, App)
timestamps()
@@ -45,12 +45,16 @@ def get_by_refresh_token(%App{id: app_id} = _app, token) do
|> Repo.find_resource()
end
+ @spec exchange_token(App.t(), Authorization.t()) ::
+ {:ok, Token.t()} | {:error, Changeset.t()}
def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth),
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(
app,
- User.get_cached_by_id(auth.user_id),
+ user,
%{scopes: auth.scopes}
)
end
@@ -81,12 +85,13 @@ defp put_valid_until(changeset, attrs) do
|> validate_required([:valid_until])
end
+ @spec create_token(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()}
def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do
%__MODULE__{user_id: user.id, app_id: app.id}
|> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes])
- |> validate_required([:scopes, :user_id, :app_id])
+ |> validate_required([:scopes, :app_id])
|> put_valid_until(attrs)
- |> put_token
+ |> put_token()
|> put_refresh_token(attrs)
|> Repo.insert()
end
diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex
new file mode 100644
index 000000000..64e78b183
--- /dev/null
+++ b/lib/pleroma/web/oauth/token/response.ex
@@ -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
diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex
index a81560a1c..7a4fddafd 100644
--- a/lib/pleroma/web/oauth/token/utils.ex
+++ b/lib/pleroma/web/oauth/token/utils.ex
@@ -3,6 +3,44 @@ defmodule Pleroma.Web.OAuth.Token.Utils do
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"
def format_created_at(%{inserted_at: inserted_at} = _token) do
inserted_at
diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex
index 166691a09..95037125d 100644
--- a/lib/pleroma/web/ostatus/activity_representer.ex
+++ b/lib/pleroma/web/ostatus/activity_representer.ex
@@ -18,15 +18,18 @@ defp get_href(id) do
end
end
- defp get_in_reply_to(%{"object" => %{"inReplyTo" => in_reply_to}}) do
- [
- {:"thr:in-reply-to",
- [ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []}
- ]
+ defp get_in_reply_to(activity) do
+ with %Object{data: %{"inReplyTo" => in_reply_to}} <- Object.normalize(activity) do
+ [
+ {:"thr:in-reply-to",
+ [ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []}
+ ]
+ else
+ _ ->
+ []
+ end
end
- defp get_in_reply_to(_), do: []
-
defp get_mentions(to) do
Enum.map(to, fn id ->
cond do
@@ -98,7 +101,7 @@ def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author)
[]}
end)
- in_reply_to = get_in_reply_to(activity.data)
+ in_reply_to = get_in_reply_to(activity)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = activity.recipients |> get_mentions
@@ -146,7 +149,6 @@ def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) d
updated_at = activity.data["published"]
inserted_at = activity.data["published"]
- _in_reply_to = get_in_reply_to(activity.data)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = activity.recipients |> get_mentions
@@ -177,7 +179,6 @@ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_autho
updated_at = activity.data["published"]
inserted_at = activity.data["published"]
- _in_reply_to = get_in_reply_to(activity.data)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
index 4744c6d83..61515b31e 100644
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -16,6 +16,7 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.OStatus.DeleteHandler
alias Pleroma.Web.OStatus.FollowHandler
alias Pleroma.Web.OStatus.NoteHandler
@@ -30,7 +31,7 @@ def is_representable?(%Activity{} = activity) do
is_nil(object) ->
false
- object.data["type"] == "Note" ->
+ Visibility.is_public?(activity) && object.data["type"] == "Note" ->
true
true ->
diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex
index f67aaf58b..0162a5be9 100644
--- a/lib/pleroma/web/rich_media/helpers.ex
+++ b/lib/pleroma/web/rich_media/helpers.ex
@@ -34,4 +34,6 @@ def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) d
end
def fetch_data_for_activity(_), do: %{}
+
+ def perform(:fetch, %Activity{} = activity), do: fetch_data_for_activity(activity)
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 6d9c77c1a..6a4e4a1d4 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -84,11 +84,13 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
- pipeline :oauth_read_or_unauthenticated do
+ pipeline :oauth_read_or_public do
plug(Pleroma.Plugs.OAuthScopesPlug, %{
scopes: ["read"],
fallback: :proceed_unauthenticated
})
+
+ plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
end
pipeline :oauth_read do
@@ -146,34 +148,60 @@ defmodule Pleroma.Web.Router do
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :oauth_write])
- post("/user/follow", AdminAPIController, :user_follow)
- post("/user/unfollow", AdminAPIController, :user_unfollow)
-
- get("/users", AdminAPIController, :list_users)
- get("/users/:nickname", AdminAPIController, :user_show)
+ post("/users/follow", AdminAPIController, :user_follow)
+ post("/users/unfollow", AdminAPIController, :user_unfollow)
+ # TODO: to be removed at version 1.0
delete("/user", AdminAPIController, :user_delete)
- patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
post("/user", AdminAPIController, :user_create)
+
+ delete("/users", AdminAPIController, :user_delete)
+ post("/users", AdminAPIController, :user_create)
+ patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
+ # TODO: to be removed at version 1.0
get("/permission_group/:nickname", AdminAPIController, :right_get)
get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get)
post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add)
delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete)
- put("/activation_status/:nickname", AdminAPIController, :set_activation_status)
+ get("/users/:nickname/permission_group", AdminAPIController, :right_get)
+ get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
+ post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
+
+ delete(
+ "/users/:nickname/permission_group/:permission_group",
+ AdminAPIController,
+ :right_delete
+ )
+
+ put("/users/:nickname/activation_status", AdminAPIController, :set_activation_status)
post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)
- get("/invite_token", AdminAPIController, :get_invite_token)
- get("/invites", AdminAPIController, :invites)
- post("/revoke_invite", AdminAPIController, :revoke_invite)
- post("/email_invite", AdminAPIController, :email_invite)
+ get("/users/invite_token", AdminAPIController, :get_invite_token)
+ get("/users/invites", AdminAPIController, :invites)
+ post("/users/revoke_invite", AdminAPIController, :revoke_invite)
+ post("/users/email_invite", AdminAPIController, :email_invite)
+ # TODO: to be removed at version 1.0
get("/password_reset", AdminAPIController, :get_password_reset)
+
+ get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
+
+ get("/users", AdminAPIController, :list_users)
+ 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
scope "/", Pleroma.Web.TwitterAPI do
@@ -197,6 +225,7 @@ defmodule Pleroma.Web.Router do
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings)
+ post("/disable_account", UtilController, :disable_account)
end
scope [] do
@@ -367,6 +396,8 @@ defmodule Pleroma.Web.Router do
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
+ post("/accounts", MastodonAPIController, :account_register)
+
get("/instance", MastodonAPIController, :masto_instance)
get("/instance/peers", MastodonAPIController, :peers)
post("/apps", MastodonAPIController, :create_app)
@@ -383,7 +414,7 @@ defmodule Pleroma.Web.Router do
get("/accounts/search", MastodonAPIController, :account_search)
scope [] do
- pipe_through(:oauth_read_or_unauthenticated)
+ pipe_through(:oauth_read_or_public)
get("/timelines/public", MastodonAPIController, :public_timeline)
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
@@ -404,7 +435,7 @@ defmodule Pleroma.Web.Router do
end
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)
end
@@ -434,7 +465,7 @@ defmodule Pleroma.Web.Router do
)
scope [] do
- pipe_through(:oauth_read_or_unauthenticated)
+ pipe_through(:oauth_read_or_public)
get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
@@ -452,7 +483,7 @@ defmodule Pleroma.Web.Router do
end
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)
@@ -466,7 +497,7 @@ defmodule Pleroma.Web.Router do
end
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)
end
@@ -650,7 +681,7 @@ defmodule Pleroma.Web.Router do
delete("/auth/sign_out", MastodonAPIController, :logout)
scope [] do
- pipe_through(:oauth_read_or_unauthenticated)
+ pipe_through(:oauth_read_or_public)
get("/web/*path", MastodonAPIController, :index)
end
end
diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex
index 0a9e51656..42709ab47 100644
--- a/lib/pleroma/web/salmon/salmon.ex
+++ b/lib/pleroma/web/salmon/salmon.ex
@@ -3,12 +3,18 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Salmon do
+ @behaviour Pleroma.Web.Federator.Publisher
+
@httpoison Application.get_env(:pleroma, :httpoison)
use Bitwise
+ alias Pleroma.Activity
alias Pleroma.Instances
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.XML
@@ -165,12 +171,12 @@ def remote_users(%{data: %{"to" => to} = data}) do
end
@doc "Pushes an activity to remote account."
- def send_to_user(%{recipient: %{info: %{salmon: salmon}}} = params),
- do: send_to_user(Map.put(params, :recipient, salmon))
+ def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
+ 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 <-
- poster.(
+ @httpoison.post(
url,
feed,
[{"Content-Type", "application/magic-envelope+xml"}]
@@ -184,11 +190,11 @@ def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is
e ->
unless params[:unreachable_since], do: Instances.set_reachable(url)
Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
- :error
+ {:error, "Unreachable instance"}
end
end
- def send_to_user(_), do: :noop
+ def publish_one(_), do: :noop
@supported_activities [
"Create",
@@ -199,13 +205,19 @@ def send_to_user(_), do: :noop
"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 """
Publishes an activity to remote accounts
"""
- @spec publish(User.t(), Pleroma.Activity.t(), Pleroma.HTTP.t()) :: none
- def publish(user, activity, poster \\ &@httpoison.post/3)
+ @spec publish(User.t(), Pleroma.Activity.t()) :: none
+ 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
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 ->
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
- Pleroma.Web.Federator.publish_single_salmon(%{
+ Publisher.enqueue_one(__MODULE__, %{
recipient: remote_user,
feed: feed,
- poster: poster,
unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
})
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
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index c03f8ab3a..489170d80 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -173,8 +173,6 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_
def config(conn, _params) do
instance = Pleroma.Config.get(:instance)
- instance_fe = Pleroma.Config.get(:fe)
- instance_chat = Pleroma.Config.get(:chat)
case get_format(conn) do
"xml" ->
@@ -219,31 +217,7 @@ def config(conn, _params) do
if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0")
}
- 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
+ pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
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 ->
String.split(line, ",") |> List.first()
end)
- |> List.delete("Account address"),
- {:ok, _} = Task.start(fn -> User.follow_import(follower, followed_identifiers) end) do
+ |> List.delete("Account address") do
+ PleromaJobQueue.enqueue(:background, User, [
+ :follow_import,
+ follower,
+ followed_identifiers
+ ])
+
json(conn, "job started")
end
end
@@ -320,8 +299,13 @@ def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
end
def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do
- with blocked_identifiers <- String.split(list),
- {:ok, _} = Task.start(fn -> User.blocks_import(blocker, blocked_identifiers) end) do
+ with blocked_identifiers <- String.split(list) do
+ PleromaJobQueue.enqueue(:background, User, [
+ :blocks_import,
+ blocker,
+ blocked_identifiers
+ ])
+
json(conn, "job started")
end
end
@@ -360,6 +344,17 @@ def delete_account(%{assigns: %{user: user}} = conn, params) do
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
json(conn, Pleroma.Captcha.new())
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 3a7774647..41e1c2877 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -128,7 +128,7 @@ def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
end
end
- def register_user(params) do
+ def register_user(params, opts \\ []) do
token = params["token"]
params = %{
@@ -162,13 +162,22 @@ def register_user(params) do
# I have no idea how this error handling works
{:error, %{error: Jason.encode!(%{captcha: [error]})}}
else
- registrations_open = Pleroma.Config.get([:instance, :registrations_open])
- registration_process(registrations_open, params, token)
+ registration_process(
+ params,
+ %{
+ registrations_open: Pleroma.Config.get([:instance, :registrations_open]),
+ token: token
+ },
+ opts
+ )
end
end
- defp registration_process(registration_open, params, token)
- when registration_open == false or is_nil(registration_open) do
+ defp registration_process(params, %{registrations_open: true}, opts) do
+ create_user(params, opts)
+ end
+
+ defp registration_process(params, %{token: token}, opts) do
invite =
unless is_nil(token) do
Repo.get_by(UserInviteToken, %{token: token})
@@ -182,19 +191,15 @@ defp registration_process(registration_open, params, token)
invite when valid_invite? ->
UserInviteToken.update_usage!(invite)
- create_user(params)
+ create_user(params, opts)
_ ->
{:error, "Expired token"}
end
end
- defp registration_process(true, params, _token) do
- create_user(params)
- end
-
- defp create_user(params) do
- changeset = User.register_changeset(%User{}, params)
+ defp create_user(params, opts) do
+ changeset = User.register_changeset(%User{}, params, opts)
case User.register(changeset) do
{:ok, user} ->
@@ -231,12 +236,15 @@ def password_reset(nickname_or_email) do
def get_user(user \\ nil, params) do
case params do
%{"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 ->
{:error, "No user with such user_id"}
- _ ->
- {:ok, target}
+ %User{info: %{deactivated: true}} ->
+ {:error, "User has been disabled"}
+
+ user ->
+ {:ok, user}
end
%{"screen_name" => nickname} ->
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 21e6c555a..31e86685a 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -101,9 +101,7 @@ def friends_timeline(%{assigns: %{user: user}} = conn, params) do
|> Map.put("blocking_user", user)
|> Map.put("user", user)
- activities =
- ActivityPub.fetch_activities([user.ap_id | user.following], params)
- |> ActivityPub.contain_timeline(user)
+ activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
conn
|> put_view(ActivityView)
@@ -440,7 +438,7 @@ def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
true <- user.local,
true <- user.info.confirmation_pending,
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),
{:ok, _} <- User.update_and_set_cache(changeset) do
conn
diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex
index d084ad734..44bcafe0e 100644
--- a/lib/pleroma/web/twitter_api/views/activity_view.ex
+++ b/lib/pleroma/web/twitter_api/views/activity_view.ex
@@ -310,7 +310,7 @@ def render(
"tags" => tags,
"activity_type" => "post",
"possibly_sensitive" => possibly_sensitive,
- "visibility" => StatusView.get_visibility(object),
+ "visibility" => Pleroma.Web.ActivityPub.Visibility.get_visibility(object),
"summary" => summary,
"summary_html" => summary |> Formatter.emojify(object.data["emoji"]),
"card" => card,
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index a3b0bf999..1239b962a 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Web.WebFinger do
alias Pleroma.User
alias Pleroma.Web
- alias Pleroma.Web.OStatus
+ alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Salmon
alias Pleroma.Web.XML
alias Pleroma.XmlBuilder
@@ -50,70 +50,40 @@ def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
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
{: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()}",
"aliases" => [user.ap_id],
- "links" => [
- %{
- "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()
- }
- ]
+ "links" => gather_links(user)
}
end
def represent_user(user, "XML") do
{: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,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"},
- {:Alias, user.ap_id},
- {:Link,
- %{
- 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()}}
- ]
+ {:Alias, user.ap_id}
+ ] ++ links
}
|> XmlBuilder.to_doc()
end
@@ -129,7 +99,7 @@ def ensure_keys_present(user) do
info_cng =
info
- |> Pleroma.User.Info.set_keys(pem)
+ |> User.Info.set_keys(pem)
cng =
Ecto.Changeset.change(user)
diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex
index 3ffa6b416..7ad0414ab 100644
--- a/lib/pleroma/web/websub/websub.ex
+++ b/lib/pleroma/web/websub/websub.ex
@@ -4,10 +4,14 @@
defmodule Pleroma.Web.Websub do
alias Ecto.Changeset
+ alias Pleroma.Activity
alias Pleroma.Instances
alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator
+ alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.FeedRepresenter
alias Pleroma.Web.Router.Helpers
@@ -18,6 +22,8 @@ defmodule Pleroma.Web.Websub do
import Ecto.Query
+ @behaviour Pleroma.Web.Federator.Publisher
+
@httpoison Application.get_env(:pleroma, :httpoison)
def verify(subscription, getter \\ &@httpoison.get/3) do
@@ -56,6 +62,13 @@ def verify(subscription, getter \\ &@httpoison.get/3) do
"Undo",
"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)
when type in @supported_activities do
response =
@@ -88,12 +101,14 @@ def publish(topic, user, %{data: %{"type" => type}} = activity)
unreachable_since: reachable_callbacks_metadata[sub.callback]
}
- Federator.publish_single_websub(data)
+ Publisher.enqueue_one(__MODULE__, data)
end)
end
def publish(_, _, _), do: ""
+ def publish(actor, activity), do: publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
+
def sign(secret, doc) do
:crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase()
end
@@ -299,4 +314,20 @@ def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret} =
{:error, response}
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
diff --git a/lib/xml_builder.ex b/lib/xml_builder.ex
index 88f8ce2a3..b58602c7b 100644
--- a/lib/xml_builder.ex
+++ b/lib/xml_builder.ex
@@ -35,6 +35,7 @@ def to_doc(content), do: ~s() <> to_xml(co
defp make_open_tag(tag, attributes) do
attributes_string =
for {attribute, value} <- attributes do
+ value = String.replace(value, "\"", """)
"#{attribute}=\"#{value}\""
end
|> Enum.join(" ")
diff --git a/mix.exs b/mix.exs
index f7955c6ca..95c052c34 100644
--- a/mix.exs
+++ b/mix.exs
@@ -13,6 +13,7 @@ def project do
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
+ test_coverage: [tool: ExCoveralls],
# Docs
name: "Pleroma",
@@ -67,7 +68,7 @@ defp deps do
{:phoenix_ecto, "~> 4.0"},
{:ecto_sql,
git: "https://github.com/elixir-ecto/ecto_sql",
- ref: "e839a9a327b632d73533ac8105ba360bc831cf83",
+ ref: "14cb065a74c488d737d973f7a91bc036c6245f78",
override: true},
{:postgrex, ">= 0.13.5"},
{:gettext, "~> 0.15"},
@@ -106,6 +107,9 @@ defp deps do
{:auto_linker,
git: "https://git.pleroma.social/pleroma/auto_linker.git",
ref: "c00c4e75b35367fa42c95ffd9b8c455bf9995829"},
+ {:http_signatures,
+ git: "https://git.pleroma.social/pleroma/http_signatures.git",
+ ref: "9789401987096ead65646b52b5a2ca6bf52fc531"},
{:pleroma_job_queue, "~> 0.2.0"},
{:telemetry, "~> 0.3"},
{:prometheus_ex, "~> 3.0"},
@@ -116,7 +120,10 @@ defp deps do
{:recon, github: "ferd/recon", tag: "2.4.0"},
{:quack, "~> 0.1.1"},
{:benchee, "~> 1.0"},
- {:esshd, "~> 0.1.0"}
+ {:esshd, "~> 0.1.0"},
+ {:ex_rated, "~> 1.2"},
+ {:plug_static_index_html, "~> 1.0.0"},
+ {:excoveralls, "~> 0.11.1", only: :test}
] ++ oauth_deps
end
diff --git a/mix.lock b/mix.lock
index d223803ab..bacc09787 100644
--- a/mix.lock
+++ b/mix.lock
@@ -21,20 +21,24 @@
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "3.1.4", "69d852da7a9f04ede725855a35ede48d158ca11a404fe94f8b2fb3b2162cd3c9", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
- "ecto_sql": {:git, "https://github.com/elixir-ecto/ecto_sql", "e839a9a327b632d73533ac8105ba360bc831cf83", [ref: "e839a9a327b632d73533ac8105ba360bc831cf83"]},
+ "ecto_sql": {:git, "https://github.com/elixir-ecto/ecto_sql", "14cb065a74c488d737d973f7a91bc036c6245f78", [ref: "14cb065a74c488d737d973f7a91bc036c6245f78"]},
"esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [: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_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_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"]},
+ "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"},
"gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "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"},
"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"},
+ "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"},
"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"},
@@ -59,6 +63,7 @@
"plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"},
"plug_cowboy": {:hex, :plug_cowboy, "2.0.1", "d798f8ee5acc86b7d42dbe4450b8b0dadf665ce588236eb0a751a132417a980e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
+ "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
@@ -78,7 +83,7 @@
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
- "tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
+ "tzdata": {:hex, :tzdata, "0.5.20", "304b9e98a02840fb32a43ec111ffbe517863c8566eb04a061f1c4dbb90b4d84c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
"unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"},
diff --git a/priv/repo/migrations/20190411094120_add_index_on_user_info_deactivated.exs b/priv/repo/migrations/20190411094120_add_index_on_user_info_deactivated.exs
new file mode 100644
index 000000000..d701dcecc
--- /dev/null
+++ b/priv/repo/migrations/20190411094120_add_index_on_user_info_deactivated.exs
@@ -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
diff --git a/priv/repo/migrations/20190511191044_set_default_state_to_reports.exs b/priv/repo/migrations/20190511191044_set_default_state_to_reports.exs
new file mode 100644
index 000000000..0d3d253b6
--- /dev/null
+++ b/priv/repo/migrations/20190511191044_set_default_state_to_reports.exs
@@ -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
diff --git a/priv/repo/migrations/20190513175809_change_hide_column_in_filter_table.exs b/priv/repo/migrations/20190513175809_change_hide_column_in_filter_table.exs
new file mode 100644
index 000000000..2ffb88cc9
--- /dev/null
+++ b/priv/repo/migrations/20190513175809_change_hide_column_in_filter_table.exs
@@ -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
diff --git a/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs b/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs
new file mode 100644
index 000000000..dc9abc998
--- /dev/null
+++ b/priv/repo/migrations/20190515222404_add_thread_visibility_function.exs
@@ -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
diff --git a/priv/static/adminfe/favicon.ico b/priv/static/adminfe/favicon.ico
new file mode 100644
index 000000000..34b63ac63
Binary files /dev/null and b/priv/static/adminfe/favicon.ico differ
diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html
new file mode 100644
index 000000000..44a58a44e
--- /dev/null
+++ b/priv/static/adminfe/index.html
@@ -0,0 +1 @@
+Admin FE
\ No newline at end of file
diff --git a/priv/static/adminfe/static/css/app.cea15678.css b/priv/static/adminfe/static/css/app.cea15678.css
new file mode 100644
index 000000000..136aa8bb1
Binary files /dev/null and b/priv/static/adminfe/static/css/app.cea15678.css differ
diff --git a/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css b/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css
new file mode 100644
index 000000000..da819ca09
Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css differ
diff --git a/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css b/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css
new file mode 100644
index 000000000..2a2fbd8e4
Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css differ
diff --git a/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css b/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css
new file mode 100644
index 000000000..7fa43bf28
Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css differ
diff --git a/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css b/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css
new file mode 100644
index 000000000..fbd926db1
Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css differ
diff --git a/priv/static/adminfe/static/css/chunk-f018.0d22684d.css b/priv/static/adminfe/static/css/chunk-f018.0d22684d.css
new file mode 100644
index 000000000..bdb738700
Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-f018.0d22684d.css differ
diff --git a/priv/static/adminfe/static/css/chunk-libs.bd17d456.css b/priv/static/adminfe/static/css/chunk-libs.bd17d456.css
new file mode 100644
index 000000000..3a7a99679
Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-libs.bd17d456.css differ
diff --git a/priv/static/adminfe/static/fonts/element-icons.2fad952.woff b/priv/static/adminfe/static/fonts/element-icons.2fad952.woff
new file mode 100644
index 000000000..28da65d49
Binary files /dev/null and b/priv/static/adminfe/static/fonts/element-icons.2fad952.woff differ
diff --git a/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf b/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf
new file mode 100644
index 000000000..73bc90f4a
Binary files /dev/null and b/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf differ
diff --git a/priv/static/adminfe/static/img/401.089007e.gif b/priv/static/adminfe/static/img/401.089007e.gif
new file mode 100644
index 000000000..cd6e0d943
Binary files /dev/null and b/priv/static/adminfe/static/img/401.089007e.gif differ
diff --git a/priv/static/adminfe/static/img/404.a57b6f3.png b/priv/static/adminfe/static/img/404.a57b6f3.png
new file mode 100644
index 000000000..3d8e2305c
Binary files /dev/null and b/priv/static/adminfe/static/img/404.a57b6f3.png differ
diff --git a/priv/static/adminfe/static/js/7zzA.e1ae1c94.js b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js
new file mode 100644
index 000000000..4387b8321
Binary files /dev/null and b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js differ
diff --git a/priv/static/adminfe/static/js/JEtC.f9ba4594.js b/priv/static/adminfe/static/js/JEtC.f9ba4594.js
new file mode 100644
index 000000000..504eaef1f
Binary files /dev/null and b/priv/static/adminfe/static/js/JEtC.f9ba4594.js differ
diff --git a/priv/static/adminfe/static/js/app.25699e3d.js b/priv/static/adminfe/static/js/app.25699e3d.js
new file mode 100644
index 000000000..54694bf5e
Binary files /dev/null and b/priv/static/adminfe/static/js/app.25699e3d.js differ
diff --git a/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js b/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js
new file mode 100644
index 000000000..1921d0f64
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js differ
diff --git a/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js b/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js
new file mode 100644
index 000000000..1b5614639
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js differ
diff --git a/priv/static/adminfe/static/js/chunk-8b70.46525646.js b/priv/static/adminfe/static/js/chunk-8b70.46525646.js
new file mode 100644
index 000000000..68b7ea1a3
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-8b70.46525646.js differ
diff --git a/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js b/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js
new file mode 100644
index 000000000..1f40d7e84
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js differ
diff --git a/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js b/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js
new file mode 100644
index 000000000..9c06e442c
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js differ
diff --git a/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js b/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js
new file mode 100644
index 000000000..db0b5dc97
Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js differ
diff --git a/priv/static/adminfe/static/js/runtime.7144b2cf.js b/priv/static/adminfe/static/js/runtime.7144b2cf.js
new file mode 100644
index 000000000..0a58ac351
Binary files /dev/null and b/priv/static/adminfe/static/js/runtime.7144b2cf.js differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js b/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js
new file mode 100644
index 000000000..e11f322cc
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css b/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css
new file mode 100644
index 000000000..128237fba
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif
new file mode 100644
index 000000000..ba90cc36f
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif
new file mode 100644
index 000000000..74d897a4f
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif
new file mode 100644
index 000000000..963a96b8a
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif
new file mode 100644
index 000000000..c7cf1011d
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif
new file mode 100644
index 000000000..716f55e16
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif
new file mode 100644
index 000000000..334d49e0e
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif
new file mode 100644
index 000000000..4efd549ed
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif
new file mode 100644
index 000000000..82c5b182e
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif
new file mode 100644
index 000000000..ca2451e10
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif
new file mode 100644
index 000000000..fe66220c2
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif
new file mode 100644
index 000000000..fd27edfaa
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif
new file mode 100644
index 000000000..0cc9bb71c
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif
new file mode 100644
index 000000000..2075dc160
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif
new file mode 100644
index 000000000..bef7e2573
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif
new file mode 100644
index 000000000..0631c7616
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif
new file mode 100644
index 000000000..648e6e879
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css b/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css
new file mode 100644
index 000000000..96e4d7c5d
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css
new file mode 100644
index 000000000..7b45d3397
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css
new file mode 100644
index 000000000..bad168cfe
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff
new file mode 100644
index 000000000..1e3be038a
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot
new file mode 100644
index 000000000..b144ba0bd
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg
new file mode 100644
index 000000000..b4ee6f408
--- /dev/null
+++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg
@@ -0,0 +1,63 @@
+
+
+
\ No newline at end of file
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf
new file mode 100644
index 000000000..a983e2dc4
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff
new file mode 100644
index 000000000..d8962df76
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot
new file mode 100644
index 000000000..5336c38ff
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg
new file mode 100644
index 000000000..9fa215f3d
--- /dev/null
+++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg
@@ -0,0 +1,131 @@
+
+
+
\ No newline at end of file
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf
new file mode 100644
index 000000000..61a48a511
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff
new file mode 100644
index 000000000..aace5d9c5
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif
new file mode 100644
index 000000000..606348c7f
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif
new file mode 100644
index 000000000..c69e93723
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif
new file mode 100644
index 000000000..cccd7f023
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif
new file mode 100644
index 000000000..388486517
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css
new file mode 100644
index 000000000..4ad815bf5
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css differ
diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map
new file mode 100644
index 000000000..c8763dcc3
--- /dev/null
+++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["./src/skins/lightgray/main/less/desktop/Reset.less","./src/skins/lightgray/main/less/desktop/Variables.less","./src/skins/lightgray/main/less/desktop/Mixins.less","./src/skins/lightgray/main/less/desktop/Animations.less","./src/skins/lightgray/main/less/desktop/TinyMCE.less","./src/skins/lightgray/main/less/desktop/CropRect.less","./src/skins/lightgray/main/less/desktop/ImagePanel.less","./src/skins/lightgray/main/less/desktop/Arrows.less","./src/skins/lightgray/main/less/desktop/Sidebar.less","./src/skins/lightgray/main/less/desktop/Container.less","./src/skins/lightgray/main/less/desktop/Scrollable.less","./src/skins/lightgray/main/less/desktop/Panel.less","./src/skins/lightgray/main/less/desktop/FloatPanel.less","./src/skins/lightgray/main/less/desktop/Window.less","./src/skins/lightgray/main/less/desktop/ToolTip.less","./src/skins/lightgray/main/less/desktop/Progress.less","./src/skins/lightgray/main/less/desktop/Notification.less","./src/skins/lightgray/main/less/desktop/AbsoluteLayout.less","./src/skins/lightgray/main/less/desktop/Button.less","./src/skins/lightgray/main/less/desktop/ButtonGroup.less","./src/skins/lightgray/main/less/desktop/Checkbox.less","./src/skins/lightgray/main/less/desktop/ComboBox.less","./src/skins/lightgray/main/less/desktop/ColorBox.less","./src/skins/lightgray/main/less/desktop/ColorButton.less","./src/skins/lightgray/main/less/desktop/ColorPicker.less","./src/skins/lightgray/main/less/desktop/Path.less","./src/skins/lightgray/main/less/desktop/FieldSet.less","./src/skins/lightgray/main/less/desktop/FitLayout.less","./src/skins/lightgray/main/less/desktop/FlowLayout.less","./src/skins/lightgray/main/less/desktop/Iframe.less","./src/skins/lightgray/main/less/desktop/InfoBox.less","./src/skins/lightgray/main/less/desktop/Label.less","./src/skins/lightgray/main/less/desktop/MenuBar.less","./src/skins/lightgray/main/less/desktop/MenuButton.less","./src/skins/lightgray/main/less/desktop/MenuItem.less","./src/skins/lightgray/main/less/desktop/Throbber.less","./src/skins/lightgray/main/less/desktop/Menu.less","./src/skins/lightgray/main/less/desktop/ListBox.less","./src/skins/lightgray/main/less/desktop/ResizeHandle.less","./src/skins/lightgray/main/less/desktop/SelectBox.less","./src/skins/lightgray/main/less/desktop/Slider.less","./src/skins/lightgray/main/less/desktop/Spacer.less","./src/skins/lightgray/main/less/desktop/SplitButton.less","./src/skins/lightgray/main/less/desktop/StackLayout.less","./src/skins/lightgray/main/less/desktop/TabPanel.less","./src/skins/lightgray/main/less/desktop/TextBox.less","./src/skins/lightgray/main/less/desktop/DropZone.less","./src/skins/lightgray/main/less/desktop/BrowseButton.less","./src/skins/lightgray/main/less/desktop/Icons.less","./src/skins/lightgray/main/less/desktop/FilePicker.less"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js b/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js
new file mode 100644
index 000000000..d7fcac80b
Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js differ
diff --git a/priv/static/images/pleroma-fox-tan-shy.png b/priv/static/images/pleroma-fox-tan-shy.png
new file mode 100644
index 000000000..6e24be1e3
Binary files /dev/null and b/priv/static/images/pleroma-fox-tan-shy.png differ
diff --git a/test/config_test.exs b/test/config_test.exs
index 0a6f0395a..73f3fcb0a 100644
--- a/test/config_test.exs
+++ b/test/config_test.exs
@@ -28,6 +28,15 @@ test "get/1 with a list of keys" do
assert Pleroma.Config.get([:azerty, :uiop], true) == true
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
assert Pleroma.Config.get!(:instance) == Application.get_env(:pleroma, :instance)
@@ -43,6 +52,15 @@ test "get!/1" do
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
Pleroma.Config.put(:config_test, true)
diff --git a/test/conversation_test.exs b/test/conversation_test.exs
index f3300e7d1..5903d10ff 100644
--- a/test/conversation_test.exs
+++ b/test/conversation_test.exs
@@ -4,11 +4,33 @@
defmodule Pleroma.ConversationTest do
use Pleroma.DataCase
+ alias Pleroma.Activity
alias Pleroma.Conversation
+ alias Pleroma.Object
alias Pleroma.Web.CommonAPI
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
assert {:ok, %Conversation{} = conversation} =
Conversation.create_for_ap_id("https://some_ap_id")
@@ -134,4 +156,40 @@ test "create_or_bump_for returns the conversation with participations" do
assert {:error, _} = Conversation.create_or_bump_for(activity)
end
+
+ test "create_or_bump_for does not normalize objects before checking the activity type" do
+ note = insert(:note)
+ note_id = note.data["id"]
+ Repo.delete(note)
+ refute Object.get_by_ap_id(note_id)
+
+ Tesla.Mock.mock(fn env ->
+ case env.url do
+ ^note_id ->
+ # TODO: add attributedTo and tag to the note factory
+ body =
+ note.data
+ |> Map.put("attributedTo", note.data["actor"])
+ |> Map.put("tag", [])
+ |> Jason.encode!()
+
+ %Tesla.Env{status: 200, body: body}
+ end
+ end)
+
+ undo = %Activity{
+ id: "fake",
+ data: %{
+ "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
+ "actor" => note.data["actor"],
+ "to" => [note.data["actor"]],
+ "object" => note_id,
+ "type" => "Undo"
+ }
+ }
+
+ Conversation.create_or_bump_for(undo)
+
+ refute Object.get_by_ap_id(note_id)
+ end
end
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index 06f4f6e50..5e7011160 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -125,7 +125,7 @@ test "gives a replacement for user links, using local nicknames in user links te
archaeme =
insert(:user, %{
nickname: "archa_eme_",
- info: %Pleroma.User.Info{source_data: %{"url" => "https://archeme/@archa_eme_"}}
+ info: %User.Info{source_data: %{"url" => "https://archeme/@archa_eme_"}}
})
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
diff --git a/test/plugs/ensure_public_or_authenticated_plug_test.exs b/test/plugs/ensure_public_or_authenticated_plug_test.exs
new file mode 100644
index 000000000..ce5d77ff7
--- /dev/null
+++ b/test/plugs/ensure_public_or_authenticated_plug_test.exs
@@ -0,0 +1,55 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors
+# 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
diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs
index 0cbb7e4b1..7dfd50c1f 100644
--- a/test/plugs/http_security_plug_test.exs
+++ b/test/plugs/http_security_plug_test.exs
@@ -7,28 +7,89 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
alias Pleroma.Config
alias Plug.Conn
- test "it sends CSP headers when enabled", %{conn: conn} do
- Config.put([:http_security, :enabled], true)
+ describe "http security enabled" do
+ setup do
+ enabled = Config.get([:http_securiy, :enabled])
- conn =
- conn
- |> get("/api/v1/instance")
+ Config.put([:http_security, :enabled], true)
- refute Conn.get_resp_header(conn, "x-xss-protection") == []
- refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == []
- refute Conn.get_resp_header(conn, "x-frame-options") == []
- refute Conn.get_resp_header(conn, "x-content-type-options") == []
- refute Conn.get_resp_header(conn, "x-download-options") == []
- refute Conn.get_resp_header(conn, "referrer-policy") == []
- refute Conn.get_resp_header(conn, "content-security-policy") == []
+ on_exit(fn ->
+ Config.put([:http_security, :enabled], enabled)
+ end)
+
+ :ok
+ end
+
+ test "it sends CSP headers when enabled", %{conn: conn} do
+ conn = get(conn, "/api/v1/instance")
+
+ refute Conn.get_resp_header(conn, "x-xss-protection") == []
+ refute Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == []
+ refute Conn.get_resp_header(conn, "x-frame-options") == []
+ refute Conn.get_resp_header(conn, "x-content-type-options") == []
+ refute Conn.get_resp_header(conn, "x-download-options") == []
+ refute Conn.get_resp_header(conn, "referrer-policy") == []
+ refute Conn.get_resp_header(conn, "content-security-policy") == []
+ end
+
+ test "it sends STS headers when enabled", %{conn: conn} do
+ Config.put([:http_security, :sts], true)
+
+ conn = get(conn, "/api/v1/instance")
+
+ refute Conn.get_resp_header(conn, "strict-transport-security") == []
+ refute Conn.get_resp_header(conn, "expect-ct") == []
+ end
+
+ test "it does not send STS headers when disabled", %{conn: conn} do
+ Config.put([:http_security, :sts], false)
+
+ conn = get(conn, "/api/v1/instance")
+
+ assert Conn.get_resp_header(conn, "strict-transport-security") == []
+ assert Conn.get_resp_header(conn, "expect-ct") == []
+ end
+
+ test "referrer-policy header reflects configured value", %{conn: conn} do
+ conn = get(conn, "/api/v1/instance")
+
+ assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"]
+
+ Config.put([:http_security, :referrer_policy], "no-referrer")
+
+ conn =
+ build_conn()
+ |> get("/api/v1/instance")
+
+ assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"]
+ end
+
+ test "it sends `report-to` & `report-uri` CSP response headers" do
+ conn =
+ build_conn()
+ |> get("/api/v1/instance")
+
+ [csp] = Conn.get_resp_header(conn, "content-security-policy")
+
+ assert csp =~ ~r|report-uri https://endpoint.com; report-to csp-endpoint;|
+
+ [reply_to] = Conn.get_resp_header(conn, "reply-to")
+
+ assert reply_to ==
+ "{\"endpoints\":[{\"url\":\"https://endpoint.com\"}],\"group\":\"csp-endpoint\",\"max-age\":10886400}"
+ end
end
test "it does not send CSP headers when disabled", %{conn: conn} do
+ enabled = Config.get([:http_securiy, :enabled])
+
Config.put([:http_security, :enabled], false)
- conn =
- conn
- |> get("/api/v1/instance")
+ on_exit(fn ->
+ Config.put([:http_security, :enabled], enabled)
+ end)
+
+ conn = get(conn, "/api/v1/instance")
assert Conn.get_resp_header(conn, "x-xss-protection") == []
assert Conn.get_resp_header(conn, "x-permitted-cross-domain-policies") == []
@@ -38,46 +99,4 @@ test "it does not send CSP headers when disabled", %{conn: conn} do
assert Conn.get_resp_header(conn, "referrer-policy") == []
assert Conn.get_resp_header(conn, "content-security-policy") == []
end
-
- test "it sends STS headers when enabled", %{conn: conn} do
- Config.put([:http_security, :enabled], true)
- Config.put([:http_security, :sts], true)
-
- conn =
- conn
- |> get("/api/v1/instance")
-
- refute Conn.get_resp_header(conn, "strict-transport-security") == []
- refute Conn.get_resp_header(conn, "expect-ct") == []
- end
-
- test "it does not send STS headers when disabled", %{conn: conn} do
- Config.put([:http_security, :enabled], true)
- Config.put([:http_security, :sts], false)
-
- conn =
- conn
- |> get("/api/v1/instance")
-
- assert Conn.get_resp_header(conn, "strict-transport-security") == []
- assert Conn.get_resp_header(conn, "expect-ct") == []
- end
-
- test "referrer-policy header reflects configured value", %{conn: conn} do
- Config.put([:http_security, :enabled], true)
-
- conn =
- conn
- |> get("/api/v1/instance")
-
- assert Conn.get_resp_header(conn, "referrer-policy") == ["same-origin"]
-
- Config.put([:http_security, :referrer_policy], "no-referrer")
-
- conn =
- build_conn()
- |> get("/api/v1/instance")
-
- assert Conn.get_resp_header(conn, "referrer-policy") == ["no-referrer"]
- end
end
diff --git a/test/plugs/http_signature_plug_test.exs b/test/plugs/http_signature_plug_test.exs
index 6a00dd4fd..efd811df7 100644
--- a/test/plugs/http_signature_plug_test.exs
+++ b/test/plugs/http_signature_plug_test.exs
@@ -4,7 +4,6 @@
defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
use Pleroma.Web.ConnCase
- alias Pleroma.Web.HTTPSignatures
alias Pleroma.Web.Plugs.HTTPSignaturePlug
import Plug.Conn
diff --git a/test/plugs/legacy_authentication_plug_test.exs b/test/plugs/legacy_authentication_plug_test.exs
index 8b0b06772..02f530058 100644
--- a/test/plugs/legacy_authentication_plug_test.exs
+++ b/test/plugs/legacy_authentication_plug_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do
- use Pleroma.Web.ConnCase, async: true
+ use Pleroma.Web.ConnCase
alias Pleroma.Plugs.LegacyAuthenticationPlug
alias Pleroma.User
diff --git a/test/plugs/rate_limit_plug_test.exs b/test/plugs/rate_limit_plug_test.exs
new file mode 100644
index 000000000..2ec9a8fb7
--- /dev/null
+++ b/test/plugs/rate_limit_plug_test.exs
@@ -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
diff --git a/test/repo_test.exs b/test/repo_test.exs
index 5382289c7..85085a1fa 100644
--- a/test/repo_test.exs
+++ b/test/repo_test.exs
@@ -1,23 +1,24 @@
defmodule Pleroma.RepoTest do
use Pleroma.DataCase
import Pleroma.Factory
+ alias Pleroma.User
describe "find_resource/1" do
test "returns user" do
user = insert(:user)
- query = from(t in Pleroma.User, where: t.id == ^user.id)
+ query = from(t in User, where: t.id == ^user.id)
assert Repo.find_resource(query) == {:ok, user}
end
test "returns not_found" do
- query = from(t in Pleroma.User, where: t.id == ^"9gBuXNpD2NyDmmxxdw")
+ query = from(t in User, where: t.id == ^"9gBuXNpD2NyDmmxxdw")
assert Repo.find_resource(query) == {:error, :not_found}
end
end
describe "get_assoc/2" do
test "get assoc from preloaded data" do
- user = %Pleroma.User{name: "Agent Smith"}
+ user = %User{name: "Agent Smith"}
token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user}
assert Repo.get_assoc(token, :user) == {:ok, user}
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 2a2954ad6..be6247ca4 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Factory do
use ExMachina.Ecto, repo: Pleroma.Repo
+ alias Pleroma.User
def participation_factory do
conversation = insert(:conversation)
@@ -23,7 +24,7 @@ def conversation_factory do
end
def user_factory do
- user = %Pleroma.User{
+ user = %User{
name: sequence(:name, &"Test テスト User #{&1}"),
email: sequence(:email, &"user#{&1}@example.com"),
nickname: sequence(:nickname, &"nick#{&1}"),
@@ -34,16 +35,16 @@ def user_factory do
%{
user
- | ap_id: Pleroma.User.ap_id(user),
- follower_address: Pleroma.User.ap_followers(user),
- following: [Pleroma.User.ap_id(user)]
+ | ap_id: User.ap_id(user),
+ follower_address: User.ap_followers(user),
+ following: [User.ap_id(user)]
}
end
def note_factory(attrs \\ %{}) do
text = sequence(:text, &"This is :moominmamma: note #{&1}")
- user = insert(:user)
+ user = attrs[:user] || insert(:user)
data = %{
"type" => "Note",
@@ -113,7 +114,8 @@ def direct_note_activity_factory do
end
def note_activity_factory(attrs \\ %{}) do
- note = attrs[:note] || insert(:note)
+ user = attrs[:user] || insert(:user)
+ note = attrs[:note] || insert(:note, user: user)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs
new file mode 100644
index 000000000..579130b05
--- /dev/null
+++ b/test/tasks/database_test.exs
@@ -0,0 +1,49 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Mix.Tasks.Pleroma.DatabaseTest do
+ alias Pleroma.Repo
+ alias Pleroma.User
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+
+ setup_all do
+ Mix.shell(Mix.Shell.Process)
+
+ on_exit(fn ->
+ Mix.shell(Mix.Shell.IO)
+ end)
+
+ :ok
+ end
+
+ describe "running update_users_following_followers_counts" do
+ test "following and followers count are updated" do
+ [user, user2] = insert_pair(:user)
+ {:ok, %User{following: following, info: info} = user} = User.follow(user, user2)
+
+ assert length(following) == 2
+ assert info.follower_count == 0
+
+ info_cng = Ecto.Changeset.change(info, %{follower_count: 3})
+
+ {:ok, user} =
+ user
+ |> Ecto.Changeset.change(%{following: following ++ following})
+ |> Ecto.Changeset.put_embed(:info, info_cng)
+ |> Repo.update()
+
+ assert length(user.following) == 4
+ assert user.info.follower_count == 3
+
+ assert :ok == Mix.Tasks.Pleroma.Database.run(["update_users_following_followers_counts"])
+
+ user = User.get_by_id(user.id)
+
+ assert length(user.following) == 2
+ assert user.info.follower_count == 0
+ end
+ end
+end
diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs
index eaf4ecf84..260ce0d95 100644
--- a/test/tasks/user_test.exs
+++ b/test/tasks/user_test.exs
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.UserTest do
+ alias Pleroma.Repo
alias Pleroma.User
use Pleroma.DataCase
@@ -338,4 +339,31 @@ test "activities are deleted" do
assert message == "User #{nickname} statuses deleted."
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
diff --git a/test/user_test.exs b/test/user_test.exs
index adc77a264..10e463ff8 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -8,6 +8,7 @@ defmodule Pleroma.UserTest do
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
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
# Use old-style integer ID to try to reproduce the problem
user = insert(:user, %{id: 1080})
- userwithnumbers = insert(:user, %{nickname: "#{user.id}garbage"})
- assert userwithnumbers == User.get_cached_by_nickname_or_id(userwithnumbers.nickname)
+ user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"})
+ assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname)
end
describe "user registration" do
@@ -276,7 +277,7 @@ test "it requires an email, name, nickname and password, bio is optional" do
end
test "it restricts certain nicknames" do
- [restricted_name | _] = Pleroma.Config.get([Pleroma.User, :restricted_nicknames])
+ [restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames])
assert is_bitstring(restricted_name)
@@ -349,7 +350,7 @@ test "it creates unconfirmed user" do
end
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?
{:ok, user} = Repo.insert(changeset)
@@ -625,6 +626,37 @@ test "it sets the info->follower_count property" do
end
end
+ describe "remove duplicates from following list" do
+ test "it removes duplicates" do
+ user = insert(:user)
+ follower = insert(:user)
+
+ {:ok, %User{following: following} = follower} = User.follow(follower, user)
+ assert length(following) == 2
+
+ {:ok, follower} =
+ follower
+ |> User.update_changeset(%{following: following ++ following})
+ |> Repo.update()
+
+ assert length(follower.following) == 4
+
+ {:ok, follower} = User.remove_duplicated_following(follower)
+ assert length(follower.following) == 2
+ end
+
+ test "it does nothing when following is uniq" do
+ user = insert(:user)
+ follower = insert(:user)
+
+ {:ok, follower} = User.follow(follower, user)
+ assert length(follower.following) == 2
+
+ {:ok, follower} = User.remove_duplicated_following(follower)
+ assert length(follower.following) == 2
+ end
+ end
+
describe "follow_import" do
test "it imports user followings from list" do
[user1, user2, user3] = insert_list(3, :user)
@@ -816,13 +848,71 @@ test "get recipients from activity" do
assert addressed in recipients
end
- test ".deactivate can de-activate then re-activate a user" do
- user = insert(:user)
- assert false == user.info.deactivated
- {:ok, user} = User.deactivate(user)
- assert true == user.info.deactivated
- {:ok, user} = User.deactivate(user, false)
- assert false == user.info.deactivated
+ describe ".deactivate" do
+ test "can de-activate then re-activate a user" do
+ user = insert(:user)
+ assert false == user.info.deactivated
+ {:ok, user} = User.deactivate(user)
+ assert true == user.info.deactivated
+ {:ok, user} = User.deactivate(user, false)
+ assert false == user.info.deactivated
+ end
+
+ test "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
test ".delete_user_activities deletes all create activities" do
@@ -1133,14 +1223,32 @@ test "follower count is updated when a follower is blocked" do
follower2 = insert(:user)
follower3 = insert(:user)
- {:ok, follower} = Pleroma.User.follow(follower, user)
- {:ok, _follower2} = Pleroma.User.follow(follower2, user)
- {:ok, _follower3} = Pleroma.User.follow(follower3, user)
+ {:ok, follower} = User.follow(follower, user)
+ {:ok, _follower2} = User.follow(follower2, user)
+ {:ok, _follower3} = User.follow(follower3, user)
- {:ok, _} = Pleroma.User.block(user, follower)
+ {:ok, _} = User.block(user, follower)
user_show = Pleroma.Web.TwitterAPI.UserView.render("show.json", %{user: user})
assert Map.get(user_show, "followers_count") == 2
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
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index 1e056b7ee..c18e0ab5f 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.ActivityPub.Utils
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)
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
activity_one = 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
})
- 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"])
- assert [public_activity, private_activity_1, private_activity_3] ==
- activities
+ assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities
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
end
end
@@ -1057,7 +1084,7 @@ test "it can create a Flag activity" do
actor = insert(:user)
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))
end
@@ -1070,7 +1097,7 @@ test "it can create a Flag activity" do
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} =
- ActivityPub.publish_one(%{
+ Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
@@ -1089,7 +1116,7 @@ test "it can create a Flag activity" do
inbox = "http://200.site/users/nick1/inbox"
assert {:ok, _} =
- ActivityPub.publish_one(%{
+ Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
@@ -1107,8 +1134,7 @@ test "it can create a Flag activity" do
actor = insert(:user)
inbox = "http://404.site/users/nick1/inbox"
- assert {:error, _} =
- ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+ assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_unreachable(inbox))
end
@@ -1120,8 +1146,7 @@ test "it can create a Flag activity" do
actor = insert(:user)
inbox = "http://connrefused.site/users/nick1/inbox"
- assert {:error, _} =
- ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+ assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_unreachable(inbox))
end
@@ -1133,7 +1158,7 @@ test "it can create a Flag activity" do
actor = insert(:user)
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))
end
@@ -1146,7 +1171,7 @@ test "it can create a Flag activity" do
inbox = "http://connrefused.site/users/nick1/inbox"
assert {:error, _} =
- ActivityPub.publish_one(%{
+ Publisher.publish_one(%{
inbox: inbox,
json: "{}",
actor: actor,
diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs
new file mode 100644
index 000000000..1e0511975
--- /dev/null
+++ b/test/web/activity_pub/mrf/simple_policy_test.exs
@@ -0,0 +1,192 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
+ use Pleroma.DataCase
+ import Pleroma.Factory
+ alias Pleroma.Config
+ alias Pleroma.Web.ActivityPub.MRF.SimplePolicy
+
+ setup do
+ orig = Config.get!(:mrf_simple)
+
+ Config.put(:mrf_simple,
+ media_removal: [],
+ media_nsfw: [],
+ federated_timeline_removal: [],
+ reject: [],
+ accept: []
+ )
+
+ on_exit(fn ->
+ Config.put(:mrf_simple, orig)
+ end)
+ end
+
+ describe "when :media_removal" do
+ test "is empty" do
+ Config.put([:mrf_simple, :media_removal], [])
+ media_message = build_media_message()
+ local_message = build_local_message()
+
+ assert SimplePolicy.filter(media_message) == {:ok, media_message}
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ end
+
+ test "has a matching host" do
+ Config.put([:mrf_simple, :media_removal], ["remote.instance"])
+ media_message = build_media_message()
+ local_message = build_local_message()
+
+ assert SimplePolicy.filter(media_message) ==
+ {:ok,
+ media_message
+ |> Map.put("object", Map.delete(media_message["object"], "attachment"))}
+
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ end
+ end
+
+ describe "when :media_nsfw" do
+ test "is empty" do
+ Config.put([:mrf_simple, :media_nsfw], [])
+ media_message = build_media_message()
+ local_message = build_local_message()
+
+ assert SimplePolicy.filter(media_message) == {:ok, media_message}
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ end
+
+ test "has a matching host" do
+ Config.put([:mrf_simple, :media_nsfw], ["remote.instance"])
+ media_message = build_media_message()
+ local_message = build_local_message()
+
+ assert SimplePolicy.filter(media_message) ==
+ {:ok,
+ media_message
+ |> put_in(["object", "tag"], ["foo", "nsfw"])
+ |> put_in(["object", "sensitive"], true)}
+
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ end
+ end
+
+ defp build_media_message do
+ %{
+ "actor" => "https://remote.instance/users/bob",
+ "type" => "Create",
+ "object" => %{
+ "attachment" => [%{}],
+ "tag" => ["foo"],
+ "sensitive" => false
+ }
+ }
+ end
+
+ describe "when :federated_timeline_removal" do
+ test "is empty" do
+ Config.put([:mrf_simple, :federated_timeline_removal], [])
+ {_, ftl_message} = build_ftl_actor_and_message()
+ local_message = build_local_message()
+
+ assert SimplePolicy.filter(ftl_message) == {:ok, ftl_message}
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ end
+
+ test "has a matching host" do
+ {actor, ftl_message} = build_ftl_actor_and_message()
+
+ ftl_message_actor_host =
+ ftl_message
+ |> Map.fetch!("actor")
+ |> URI.parse()
+ |> Map.fetch!(:host)
+
+ Config.put([:mrf_simple, :federated_timeline_removal], [ftl_message_actor_host])
+ local_message = build_local_message()
+
+ assert {:ok, ftl_message} = SimplePolicy.filter(ftl_message)
+ assert actor.follower_address in ftl_message["to"]
+ refute actor.follower_address in ftl_message["cc"]
+ refute "https://www.w3.org/ns/activitystreams#Public" in ftl_message["to"]
+ assert "https://www.w3.org/ns/activitystreams#Public" in ftl_message["cc"]
+
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ end
+ end
+
+ defp build_ftl_actor_and_message do
+ actor = insert(:user)
+
+ {actor,
+ %{
+ "actor" => actor.ap_id,
+ "to" => ["https://www.w3.org/ns/activitystreams#Public", "http://foo.bar/baz"],
+ "cc" => [actor.follower_address, "http://foo.bar/qux"]
+ }}
+ end
+
+ describe "when :reject" do
+ test "is empty" do
+ Config.put([:mrf_simple, :reject], [])
+
+ remote_message = build_remote_message()
+
+ assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
+ end
+
+ test "has a matching host" do
+ Config.put([:mrf_simple, :reject], ["remote.instance"])
+
+ remote_message = build_remote_message()
+
+ assert SimplePolicy.filter(remote_message) == {:reject, nil}
+ end
+ end
+
+ describe "when :accept" do
+ test "is empty" do
+ Config.put([:mrf_simple, :accept], [])
+
+ local_message = build_local_message()
+ remote_message = build_remote_message()
+
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
+ end
+
+ test "is not empty but it doesn't have a matching host" do
+ Config.put([:mrf_simple, :accept], ["non.matching.remote"])
+
+ local_message = build_local_message()
+ remote_message = build_remote_message()
+
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ assert SimplePolicy.filter(remote_message) == {:reject, nil}
+ end
+
+ test "has a matching host" do
+ Config.put([:mrf_simple, :accept], ["remote.instance"])
+
+ local_message = build_local_message()
+ remote_message = build_remote_message()
+
+ assert SimplePolicy.filter(local_message) == {:ok, local_message}
+ assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
+ end
+ end
+
+ defp build_local_message do
+ %{
+ "actor" => "#{Pleroma.Web.base_url()}/users/alice",
+ "to" => [],
+ "cc" => []
+ }
+ end
+
+ defp build_remote_message do
+ %{"actor" => "https://remote.instance/users/bob"}
+ end
+end
diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs
index 24b96c4aa..e2584f635 100644
--- a/test/web/activity_pub/visibilty_test.exs
+++ b/test/web/activity_pub/visibilty_test.exs
@@ -95,4 +95,26 @@ test "visible_for_user?", %{
refute Visibility.visible_for_user?(private, unrelated)
refute Visibility.visible_for_user?(direct, unrelated)
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
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index f433f6be2..ca12c7215 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -5,11 +5,13 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase
+ alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.UserInviteToken
+ alias Pleroma.Web.CommonAPI
import Pleroma.Factory
- describe "/api/pleroma/admin/user" do
+ describe "/api/pleroma/admin/users" do
test "Delete" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
@@ -18,7 +20,7 @@ test "Delete" do
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
- |> delete("/api/pleroma/admin/user?nickname=#{user.nickname}")
+ |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
assert json_response(conn, 200) == user.nickname
end
@@ -30,7 +32,7 @@ test "Create" do
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
- |> post("/api/pleroma/admin/user", %{
+ |> post("/api/pleroma/admin/users", %{
"nickname" => "lain",
"email" => "lain@example.org",
"password" => "test"
@@ -75,7 +77,7 @@ test "when the user doesn't exist", %{conn: conn} do
end
end
- describe "/api/pleroma/admin/user/follow" do
+ describe "/api/pleroma/admin/users/follow" do
test "allows to force-follow another user" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
@@ -84,7 +86,7 @@ test "allows to force-follow another user" do
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
- |> post("/api/pleroma/admin/user/follow", %{
+ |> post("/api/pleroma/admin/users/follow", %{
"follower" => follower.nickname,
"followed" => user.nickname
})
@@ -96,7 +98,7 @@ test "allows to force-follow another user" do
end
end
- describe "/api/pleroma/admin/user/unfollow" do
+ describe "/api/pleroma/admin/users/unfollow" do
test "allows to force-unfollow another user" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
@@ -107,7 +109,7 @@ test "allows to force-unfollow another user" do
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
- |> post("/api/pleroma/admin/user/unfollow", %{
+ |> post("/api/pleroma/admin/users/unfollow", %{
"follower" => follower.nickname,
"followed" => user.nickname
})
@@ -191,7 +193,7 @@ test "it does not modify tags of not specified users", %{conn: conn, user3: user
end
end
- describe "/api/pleroma/admin/permission_group" do
+ describe "/api/pleroma/admin/users/:nickname/permission_group" do
test "GET is giving user_info" do
admin = insert(:user, info: %{is_admin: true})
@@ -199,7 +201,7 @@ test "GET is giving user_info" do
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
- |> get("/api/pleroma/admin/permission_group/#{admin.nickname}")
+ |> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/")
assert json_response(conn, 200) == %{
"is_admin" => true,
@@ -215,7 +217,7 @@ test "/:right POST, can add to a permission group" do
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
- |> post("/api/pleroma/admin/permission_group/#{user.nickname}/admin")
+ |> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
assert json_response(conn, 200) == %{
"is_admin" => true
@@ -230,7 +232,7 @@ test "/:right DELETE, can remove from a permission group" do
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
- |> delete("/api/pleroma/admin/permission_group/#{user.nickname}/admin")
+ |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin")
assert json_response(conn, 200) == %{
"is_admin" => false
@@ -238,7 +240,7 @@ test "/:right DELETE, can remove from a permission group" do
end
end
- describe "PUT /api/pleroma/admin/activation_status" do
+ describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
@@ -255,7 +257,7 @@ test "deactivates the user", %{conn: conn} do
conn =
conn
- |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: false})
+ |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
user = User.get_cached_by_id(user.id)
assert user.info.deactivated == true
@@ -267,7 +269,7 @@ test "activates the user", %{conn: conn} do
conn =
conn
- |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: true})
+ |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true})
user = User.get_cached_by_id(user.id)
assert user.info.deactivated == false
@@ -280,7 +282,7 @@ test "returns 403 when requested by a non-admin", %{conn: conn} do
conn =
conn
|> assign(:user, user)
- |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: false})
+ |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
assert json_response(conn, :forbidden)
end
@@ -309,7 +311,9 @@ test "sends invitation and returns 204", %{conn: conn, user: user} do
conn =
conn
|> assign(:user, user)
- |> post("/api/pleroma/admin/email_invite?email=#{recipient_email}&name=#{recipient_name}")
+ |> post(
+ "/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}"
+ )
assert json_response(conn, :no_content)
@@ -341,13 +345,13 @@ test "it returns 403 if requested by a non-admin", %{conn: conn} do
conn =
conn
|> assign(:user, non_admin_user)
- |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD")
+ |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :forbidden)
end
end
- describe "POST /api/pleroma/admin/email_invite, with invalid config" do
+ describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do
setup do
[user: insert(:user, info: %{is_admin: true})]
end
@@ -367,7 +371,7 @@ test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: u
conn =
conn
|> assign(:user, user)
- |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD")
+ |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
@@ -387,7 +391,7 @@ test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: us
conn =
conn
|> assign(:user, user)
- |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD")
+ |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
@@ -405,7 +409,7 @@ test "/api/pleroma/admin/invite_token" do
assert conn.status == 200
end
- test "/api/pleroma/admin/password_reset" do
+ test "/api/pleroma/admin/users/:nickname/password_reset" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
@@ -413,7 +417,7 @@ test "/api/pleroma/admin/password_reset" do
build_conn()
|> assign(:user, admin)
|> put_req_header("accept", "application/json")
- |> get("/api/pleroma/admin/password_reset?nickname=#{user.nickname}")
+ |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset")
assert conn.status == 200
end
@@ -822,7 +826,7 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
}
end
- describe "GET /api/pleroma/admin/invite_token" do
+ describe "GET /api/pleroma/admin/users/invite_token" do
setup do
admin = insert(:user, info: %{is_admin: true})
@@ -834,7 +838,7 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
end
test "without options", %{conn: conn} do
- conn = get(conn, "/api/pleroma/admin/invite_token")
+ conn = get(conn, "/api/pleroma/admin/users/invite_token")
token = json_response(conn, 200)
invite = UserInviteToken.find_by_token!(token)
@@ -846,7 +850,7 @@ test "without options", %{conn: conn} do
test "with expires_at", %{conn: conn} do
conn =
- get(conn, "/api/pleroma/admin/invite_token", %{
+ get(conn, "/api/pleroma/admin/users/invite_token", %{
"invite" => %{"expires_at" => Date.to_string(Date.utc_today())}
})
@@ -861,7 +865,7 @@ test "with expires_at", %{conn: conn} do
test "with max_use", %{conn: conn} do
conn =
- get(conn, "/api/pleroma/admin/invite_token", %{
+ get(conn, "/api/pleroma/admin/users/invite_token", %{
"invite" => %{"max_use" => 150}
})
@@ -875,7 +879,7 @@ test "with max_use", %{conn: conn} do
test "with max use and expires_at", %{conn: conn} do
conn =
- get(conn, "/api/pleroma/admin/invite_token", %{
+ get(conn, "/api/pleroma/admin/users/invite_token", %{
"invite" => %{"max_use" => 150, "expires_at" => Date.to_string(Date.utc_today())}
})
@@ -888,7 +892,7 @@ test "with max use and expires_at", %{conn: conn} do
end
end
- describe "GET /api/pleroma/admin/invites" do
+ describe "GET /api/pleroma/admin/users/invites" do
setup do
admin = insert(:user, info: %{is_admin: true})
@@ -900,7 +904,7 @@ test "with max use and expires_at", %{conn: conn} do
end
test "no invites", %{conn: conn} do
- conn = get(conn, "/api/pleroma/admin/invites")
+ conn = get(conn, "/api/pleroma/admin/users/invites")
assert json_response(conn, 200) == %{"invites" => []}
end
@@ -908,7 +912,7 @@ test "no invites", %{conn: conn} do
test "with invite", %{conn: conn} do
{:ok, invite} = UserInviteToken.create_invite()
- conn = get(conn, "/api/pleroma/admin/invites")
+ conn = get(conn, "/api/pleroma/admin/users/invites")
assert json_response(conn, 200) == %{
"invites" => [
@@ -926,7 +930,7 @@ test "with invite", %{conn: conn} do
end
end
- describe "POST /api/pleroma/admin/revoke_invite" do
+ describe "POST /api/pleroma/admin/users/revoke_invite" do
test "with token" do
admin = insert(:user, info: %{is_admin: true})
{:ok, invite} = UserInviteToken.create_invite()
@@ -934,7 +938,7 @@ test "with token" do
conn =
build_conn()
|> assign(:user, admin)
- |> post("/api/pleroma/admin/revoke_invite", %{"token" => invite.token})
+ |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token})
assert json_response(conn, 200) == %{
"expires_at" => nil,
@@ -947,4 +951,329 @@ test "with token" do
}
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
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index a5b07c446..696060fb1 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -87,6 +87,28 @@ test "it filters out obviously bad tags when accepting a post as Markdown" do
assert object.data["content"] == "2hu
alert('xss')"
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
describe "reactions" do
@@ -239,10 +261,41 @@ test "creates a report" do
data: %{
"type" => "Flag",
"content" => ^comment,
- "object" => [^target_ap_id, ^activity_ap_id]
+ "object" => [^target_ap_id, ^activity_ap_id],
+ "state" => "open"
}
} = flag_activity
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
describe "reblog muting" do
@@ -257,14 +310,14 @@ test "creates a report" do
test "add a reblog mute", %{muter: muter, muted: muted} do
{:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
- assert Pleroma.User.showing_reblogs?(muter, muted) == false
+ assert User.showing_reblogs?(muter, muted) == false
end
test "remove a reblog mute", %{muter: muter, muted: muted} do
{:ok, muter} = CommonAPI.hide_reblogs(muter, muted)
{:ok, muter} = CommonAPI.show_reblogs(muter, muted)
- assert Pleroma.User.showing_reblogs?(muter, muted) == true
+ assert User.showing_reblogs?(muter, muted) == true
end
end
end
diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs
index 52729eb50..0f43bc8f2 100644
--- a/test/web/federator_test.exs
+++ b/test/web/federator_test.exs
@@ -58,7 +58,7 @@ test "with relays deactivated, it does not publish to the relay", %{
describe "Targets reachability filtering in `publish`" do
test_with_mock "it federates only to reachable instances via AP",
- Federator,
+ Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
user = insert(:user)
@@ -88,13 +88,18 @@ test "with relays deactivated, it does not publish to the relay", %{
{:ok, _activity} =
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
test_with_mock "it federates only to reachable instances via Websub",
- Federator,
+ Pleroma.Web.Websub,
[:passthrough],
[] do
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"})
assert called(
- Federator.publish_single_websub(%{
+ Pleroma.Web.Websub.publish_one(%{
callback: sub2.callback,
unreachable_since: dt
})
)
- refute called(Federator.publish_single_websub(%{callback: sub1.callback}))
+ refute called(Pleroma.Web.Websub.publish_one(%{callback: sub1.callback}))
end
test_with_mock "it federates only to reachable instances via Salmon",
- Federator,
+ Pleroma.Web.Salmon,
[:passthrough],
[] do
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!"})
assert called(
- Federator.publish_single_salmon(%{
+ Pleroma.Web.Salmon.publish_one(%{
recipient: remote_user2,
unreachable_since: dt
})
)
- refute called(Federator.publish_single_websub(%{recipient: remote_user1}))
+ refute called(Pleroma.Web.Salmon.publish_one(%{recipient: remote_user1}))
end
end
diff --git a/test/web/http_sigs/http_sig_test.exs b/test/web/http_sigs/http_sig_test.exs
deleted file mode 100644
index c4d2eaf78..000000000
--- a/test/web/http_sigs/http_sig_test.exs
+++ /dev/null
@@ -1,194 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
-# 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
diff --git a/test/web/http_sigs/priv.key b/test/web/http_sigs/priv.key
deleted file mode 100644
index 425518a06..000000000
--- a/test/web/http_sigs/priv.key
+++ /dev/null
@@ -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-----
diff --git a/test/web/http_sigs/pub.key b/test/web/http_sigs/pub.key
deleted file mode 100644
index b3bbf6cb9..000000000
--- a/test/web/http_sigs/pub.key
+++ /dev/null
@@ -1,6 +0,0 @@
------BEGIN PUBLIC KEY-----
-MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
-6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6
-Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw
-oYi+1hqp1fIekaxsyQIDAQAB
------END PUBLIC KEY-----
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 505e45010..cbff141c8 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.OAuth.App
+ alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OStatus
alias Pleroma.Web.Push
alias Pleroma.Web.TwitterAPI.TwitterAPI
@@ -80,6 +81,19 @@ test "the public timeline", %{conn: conn} do
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
user = insert(:user)
@@ -432,7 +446,7 @@ test "verify_credentials", %{conn: conn} do
end
test "verify_credentials default scope unlisted", %{conn: conn} do
- user = insert(:user, %{info: %Pleroma.User.Info{default_scope: "unlisted"}})
+ user = insert(:user, %{info: %User.Info{default_scope: "unlisted"}})
conn =
conn
@@ -572,6 +586,7 @@ test "creating a filter", %{conn: conn} do
assert response = json_response(conn, 200)
assert response["phrase"] == filter.phrase
assert response["context"] == filter.context
+ assert response["irreversible"] == false
assert response["id"] != nil
assert response["id"] != ""
end
@@ -1307,7 +1322,7 @@ test "returns the relationships for the current user", %{conn: conn} do
describe "locked accounts" do
test "/api/v1/follow_requests works" do
- user = insert(:user, %{info: %Pleroma.User.Info{locked: true}})
+ user = insert(:user, %{info: %User.Info{locked: true}})
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
@@ -1352,7 +1367,7 @@ test "/api/v1/follow_requests/:id/authorize works" do
end
test "verify_credentials", %{conn: conn} do
- user = insert(:user, %{info: %Pleroma.User.Info{default_scope: "private"}})
+ user = insert(:user, %{info: %User.Info{default_scope: "private"}})
conn =
conn
@@ -1364,7 +1379,7 @@ test "verify_credentials", %{conn: conn} do
end
test "/api/v1/follow_requests/:id/reject works" do
- user = insert(:user, %{info: %Pleroma.User.Info{locked: true}})
+ user = insert(:user, %{info: %User.Info{locked: true}})
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
@@ -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")
|> json_response(:ok)
- assert length(anonymous_response) == 0
+ assert Enum.empty?(anonymous_response)
end
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")
|> json_response(:ok)
- assert length(response) == 0
+ assert Enum.empty?(response)
end
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"])
assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
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
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index cb6836983..1c04ac9ad 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -614,6 +614,27 @@ test "issues a token for request with HTTP basic auth client credentials" do
assert token.scopes == ["scope1", "scope2"]
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
user = insert(:user)
app = insert(:oauth_app)
@@ -644,7 +665,7 @@ test "rejects token exchange for valid credentials belonging to unconfirmed user
password = "testpassword"
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} =
user
diff --git a/test/web/oauth/token/utils_test.exs b/test/web/oauth/token/utils_test.exs
new file mode 100644
index 000000000..20e338cab
--- /dev/null
+++ b/test/web/oauth/token/utils_test.exs
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors
+# 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
diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs
index a4bb68c4d..16ee02abb 100644
--- a/test/web/ostatus/activity_representer_test.exs
+++ b/test/web/ostatus/activity_representer_test.exs
@@ -67,37 +67,31 @@ test "a note activity" do
end
test "a reply note" do
- note = insert(:note_activity)
- answer = insert(:note_activity)
- object = answer.data["object"]
- object = Map.put(object, "inReplyTo", note.data["object"]["id"])
-
- data = %{answer.data | "object" => object}
- answer = %{answer | data: data}
-
- note_object = Object.get_by_ap_id(note.data["object"]["id"])
+ user = insert(:user)
+ note_object = insert(:note)
+ _note = insert(:note_activity, %{note: note_object})
+ object = insert(:note, %{data: %{"inReplyTo" => note_object.data["id"]}})
+ answer = insert(:note_activity, %{note: object})
Repo.update!(
Object.change(note_object, %{data: Map.put(note_object.data, "external_url", "someurl")})
)
- user = User.get_cached_by_ap_id(answer.data["actor"])
-
expected = """
http://activitystrea.ms/schema/1.0/note
http://activitystrea.ms/schema/1.0/post
- #{answer.data["object"]["id"]}
+ #{object.data["id"]}
New note by #{user.nickname}
- #{answer.data["object"]["content"]}
- #{answer.data["object"]["published"]}
- #{answer.data["object"]["published"]}
+ #{object.data["content"]}
+ #{object.data["published"]}
+ #{object.data["published"]}
#{answer.data["context"]}
2hu
-
-
+
+
-
+
"""
diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs
index 2916caf8d..f6be16862 100644
--- a/test/web/ostatus/ostatus_test.exs
+++ b/test/web/ostatus/ostatus_test.exs
@@ -355,7 +355,7 @@ test "tries to use the information in poco fields" do
{:ok, user} = OStatus.find_or_make_user(uri)
- user = Pleroma.User.get_cached_by_id(user.id)
+ user = User.get_cached_by_id(user.id)
assert user.name == "Constance Variable"
assert user.nickname == "lambadalambda@social.heldscal.la"
assert user.local == false
@@ -374,7 +374,7 @@ test "find_or_make_user sets all the nessary input fields" do
{:ok, user} = OStatus.find_or_make_user(uri)
assert user.info ==
- %Pleroma.User.Info{
+ %User.Info{
id: user.info.id,
ap_enabled: false,
background: %{},
@@ -407,7 +407,7 @@ test "find_make_or_update_user takes an author element and returns an updated us
{:ok, user} = OStatus.find_or_make_user(uri)
old_name = user.name
old_bio = user.bio
- change = Ecto.Changeset.change(user, %{avatar: nil, bio: nil, old_name: nil})
+ change = Ecto.Changeset.change(user, %{avatar: nil, bio: nil, name: nil})
{:ok, user} = Repo.update(change)
refute user.avatar
diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs
index 7532578ca..232082779 100644
--- a/test/web/salmon/salmon_test.exs
+++ b/test/web/salmon/salmon_test.exs
@@ -7,7 +7,9 @@ defmodule Pleroma.Web.Salmon.SalmonTest do
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Salmon
+ import Mock
import Pleroma.Factory
@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"
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 = %{
info: %{
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"])
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
- poster = fn url, _data, _headers ->
- assert url == "http://test-example.org/salmon"
- end
+ Salmon.publish(user, activity)
- Salmon.publish(user, activity, poster)
+ assert called(Publisher.enqueue_one(Salmon, %{recipient: mentioned_user}))
end
end
diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs
index 90718cfb4..e194f14fb 100644
--- a/test/web/twitter_api/twitter_api_controller_test.exs
+++ b/test/web/twitter_api/twitter_api_controller_test.exs
@@ -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
setup do
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} =
user
@@ -1145,7 +1145,7 @@ test "it returns 500 if token is invalid", %{conn: conn, user: user} do
end
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} =
user
diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs
index 56474447b..2cd82b3e7 100644
--- a/test/web/twitter_api/util_controller_test.exs
+++ b/test/web/twitter_api/util_controller_test.exs
@@ -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
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 =
conn
@@ -157,29 +157,7 @@ test "it returns the managed config", %{conn: conn} do
|> get("/api/statusnet/config.json")
|> json_response(:ok)
- assert response["site"]["pleromafe"]
- 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"
+ assert response["site"]["pleromafe"] == %{"theme" => "asuka-hospital"}
end
end
@@ -251,4 +229,22 @@ test "GET /api/pleroma/healthcheck", %{conn: conn} do
assert conn.status in [200, 503]
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
diff --git a/test/web/views/error_view_test.exs b/test/web/views/error_view_test.exs
index d529fd2c3..3857d585f 100644
--- a/test/web/views/error_view_test.exs
+++ b/test/web/views/error_view_test.exs
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.ErrorViewTest do
use Pleroma.Web.ConnCase, async: true
+ import ExUnit.CaptureLog
# Bring render/3 and render_to_string/3 for testing custom views
import Phoenix.View
@@ -13,17 +14,23 @@ test "renders 404.json" do
end
test "render 500.json" do
- assert render(Pleroma.Web.ErrorView, "500.json", []) ==
- %{errors: %{detail: "Internal server error", reason: "nil"}}
+ assert capture_log(fn ->
+ assert render(Pleroma.Web.ErrorView, "500.json", []) ==
+ %{errors: %{detail: "Internal server error", reason: "nil"}}
+ end) =~ "[error] Internal server error: nil"
end
test "render any other" do
- assert render(Pleroma.Web.ErrorView, "505.json", []) ==
- %{errors: %{detail: "Internal server error", reason: "nil"}}
+ assert capture_log(fn ->
+ assert render(Pleroma.Web.ErrorView, "505.json", []) ==
+ %{errors: %{detail: "Internal server error", reason: "nil"}}
+ end) =~ "[error] Internal server error: nil"
end
test "render 500.json with reason" do
- assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") ==
- %{errors: %{detail: "Internal server error", reason: "\"test reason\""}}
+ assert capture_log(fn ->
+ 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