Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into rum-index

This commit is contained in:
lain 2019-05-17 12:26:59 +02:00
commit 412a3d8a0f
173 changed files with 4238 additions and 1179 deletions

View file

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

View file

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

View file

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

View file

@ -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 <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>.
If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>.
## Installation

View file

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

View file

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

View file

@ -24,7 +24,7 @@ Authentication is required and the user must be an admin.
- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com`
- 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: Users 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: Users 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 doesnt 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": "<span class=\"h-card\"><a data-user=\"9hEkA5JsvAdlSrocam\" class=\"u-url mention\" href=\"https://pleroma.example.org/users/lain\">@<span>lain</span></a></span> click on my link <a href=\"https://www.google.com/\">https://www.google.com/</a>",
"created_at": "2019-04-23T19:15:47.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9i6mQ9uVrrOmOime8m",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "lain",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/lain",
"username": "lain"
},
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "@lain click on my link https://www.google.com/"
},
"conversation_id": 28,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/8717b90f-8e09-4b58-97b0-e3305472b396",
"url": "https://pleroma.example.org/notice/9i6mQ9uVrrOmOime8m",
"visibility": "direct"
}
]
}
]
}
```
## `/api/pleroma/admin/reports/:id`
### Get an individual report
- Method `GET`
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id`
### Change the state of the report
- Method `PUT`
- Params:
- `id`
- `state`: required, the new state. Valid values are `open`, `closed` and `resolved`
- Response:
- On failure:
- 400 Bad Request `"Unsupported state"`
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Report object (see above)
## `/api/pleroma/admin/reports/:id/respond`
### Respond to a report
- Method `POST`
- Params:
- `id`
- `status`: required, the message
- Response:
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, created Mastodon Status entity
```json
{
"account": { ... },
"application": {
"name": "Web",
"website": null
},
"bookmarked": false,
"card": null,
"content": "Your claim is going to be closed",
"created_at": "2019-05-11T17:13:03.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9ihuiSL1405I65TmEq",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
{
"acct": "admin",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/admin",
"username": "admin"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "Your claim is going to be closed"
},
"conversation_id": 35,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/cab0836d-9814-46cd-a0ea-529da9db5fcb",
"url": "https://pleroma.example.org/notice/9ihuiSL1405I65TmEq",
"visibility": "direct"
}
```
## `/api/pleroma/admin/statuses/:id`
### Change the scope of an individual reported status
- Method `PUT`
- Params:
- `id`
- `sensitive`: optional, valid values are `true` or `false`
- `visibility`: optional, valid values are `public`, `private` and `unlisted`
- Response:
- On failure:
- 400 Bad Request `"Unsupported visibility"`
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: JSON, Mastodon Status entity
## `/api/pleroma/admin/statuses/:id`
### Delete an individual reported status
- Method `DELETE`
- Params:
- `id`
- Response:
- On failure:
- 403 Forbidden `{"error": "error_msg"}`
- 404 Not Found `"Not found"`
- On success: 200 OK `{}`

View file

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

View file

@ -61,6 +61,15 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
* Response: JSON. Returns `{"status": "success"}` if the deletion was successful, `{"error": "[error message]"}` otherwise
* 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`

View file

@ -105,6 +105,12 @@ config :pleroma, Pleroma.Emails.Mailer,
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`)
* `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

View file

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

View file

@ -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 (バックエンド) をインストールします

View file

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

View file

@ -137,7 +137,7 @@ def run(["get-packs" | args]) do
])
)
files = Tesla.get!(client(), files_url).body |> Poison.decode!()
files = Tesla.get!(client(), files_url).body |> Jason.decode!()
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"], "<br>", "\r")))
info(HTML.strip_tags(String.replace(object.data["content"], "<br>", "\r")))
end)
|> Enum.join("i\tfake\t(NULL)\t0\r\n")
end

View file

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

View file

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

View file

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

View file

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

View file

@ -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")
@ -21,22 +22,43 @@ def call(%{params: %{"access_token" => access_token}} = conn, _) 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(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
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
# Gets user by token
#
@spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil
@ -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()}

View file

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

41
lib/pleroma/signature.ex Normal file
View file

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Signature do
@behaviour HTTPSignatures.Adapter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
def fetch_public_key(conn) do
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def refetch_public_key(conn) do
with actor_id <- Utils.get_ap_id(conn.params["actor"]),
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def sign(%User{} = user, headers) do
with {:ok, %{info: %{keys: keys}}} <- WebFinger.ensure_keys_present(user),
{:ok, private_key, _} <- Salmon.keys_from_pem(keys) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
end
end
end

View file

@ -14,7 +14,7 @@ def process_url(url) do
def process_response_body(body) do
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,

View file

@ -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)
user
|> change()
|> put_embed(:info, info_cng)
update_and_set_cache(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)
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

View file

@ -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, %{
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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,152 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Instances
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Web.ActivityPub.Visibility
@behaviour Pleroma.Web.Federator.Publisher
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
@moduledoc """
ActivityPub outgoing federation module.
"""
@doc """
Determine if an activity can be represented by running it through Transmogrifier.
"""
def is_representable?(%Activity{} = activity) do
with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
true
else
_e ->
false
end
end
@doc """
Publish a single message to a peer. Takes a struct with the following
parameters set:
* `inbox`: the inbox to publish to
* `json`: the JSON message body representing the ActivityPub message
* `actor`: the actor which is signing the message
* `id`: the ActivityStreams URI of the message
"""
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date =
NaiveDateTime.utc_now()
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
signature =
Pleroma.Signature.sign(actor, %{
host: host,
"content-length": byte_size(json),
digest: digest,
date: date
})
with {:ok, %{status: code}} when code in 200..299 <-
result =
@httpoison.post(
inbox,
json,
[
{"Content-Type", "application/activity+json"},
{"Date", date},
{"signature", signature},
{"digest", digest}
]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(inbox)
result
else
{_post_result, response} ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
{:error, response}
end
end
defp should_federate?(inbox, public) do
if public do
true
else
inbox_info = URI.parse(inbox)
!Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
end
@doc """
Publishes an activity to all relevant peers.
"""
def publish(%User{} = actor, %Activity{} = activity) do
remote_followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
public = is_public?(activity)
if public && Config.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
end
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{source_data: data}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
Pleroma.Web.Federator.Publisher.enqueue_one(
__MODULE__,
%{
inbox: inbox,
json: json,
actor: actor,
id: activity.data["id"],
unreachable_since: unreachable_since
}
)
end)
end
def gather_webfinger_links(%User{} = user) do
[
%{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
%{
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href" => user.ap_id
}
]
end
def gather_nodeinfo_protocol_names, do: ["activitypub"]
end

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ReportView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{reports: reports}) do
%{
reports: render_many(reports, __MODULE__, "show.json", as: :report)
}
end
def render("show.json", %{report: report}) do
user = User.get_cached_by_ap_id(report.data["actor"])
created_at = Utils.to_masto_date(report.data["published"])
[account_ap_id | status_ap_ids] = report.data["object"]
account = User.get_cached_by_ap_id(account_ap_id)
statuses =
Enum.map(status_ap_ids, fn ap_id ->
Activity.get_by_ap_id_with_object(ap_id)
end)
%{
id: report.id,
account: AccountView.render("account.json", %{user: account}),
actor: AccountView.render("account.json", %{user: user}),
content: report.data["content"],
created_at: created_at,
statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),
state: report.data["state"]
}
end
end

View file

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

View file

@ -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"
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
end
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
def get_visibility(_), do: "public"
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 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,12 +39,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.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)

View file

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

View file

@ -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
"<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.User
alias Pleroma.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: []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,44 @@ defmodule Pleroma.Web.OAuth.Token.Utils do
Auxiliary functions for dealing with tokens.
"""
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

View file

@ -18,14 +18,17 @@ defp get_href(id) do
end
end
defp get_in_reply_to(%{"object" => %{"inReplyTo" => in_reply_to}}) do
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 ->
@ -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"])

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
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)
},
defp gather_links(%User{} = user) do
[
%{
"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()
}
]
] ++ Publisher.gather_webfinger_links(user)
end
def represent_user(user, "JSON") do
{:ok, user} = ensure_keys_present(user)
%{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
"aliases" => [user.ap_id],
"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)

View file

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

View file

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

11
mix.exs
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -0,0 +1 @@
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=static/css/chunk-elementUI.4296cedf.css rel=stylesheet><link href=static/css/chunk-libs.bd17d456.css rel=stylesheet><link href=static/css/app.cea15678.css rel=stylesheet></head><body><script src=/pleroma/admin/static/tinymce4.7.5/tinymce.min.js></script><div id=app></div><script type=text/javascript src=static/js/runtime.7144b2cf.js></script><script type=text/javascript src=static/js/chunk-elementUI.d388c21d.js></script><script type=text/javascript src=static/js/chunk-libs.48e79a9e.js></script><script type=text/javascript src=static/js/app.25699e3d.js></script></body></html>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
.errPage-container[data-v-ab9be52c]{width:800px;max-width:100%;margin:100px auto}.errPage-container .pan-back-btn[data-v-ab9be52c]{background:#008489;color:#fff;border:none!important}.errPage-container .pan-gif[data-v-ab9be52c]{margin:0 auto;display:block}.errPage-container .pan-img[data-v-ab9be52c]{display:block;margin:0 auto;width:100%}.errPage-container .text-jumbo[data-v-ab9be52c]{font-size:60px;font-weight:700;color:#484848}.errPage-container .list-unstyled[data-v-ab9be52c]{font-size:14px}.errPage-container .list-unstyled li[data-v-ab9be52c]{padding-bottom:5px}.errPage-container .list-unstyled a[data-v-ab9be52c]{color:#008489;text-decoration:none}.errPage-container .list-unstyled a[data-v-ab9be52c]:hover{text-decoration:underline}

View file

@ -0,0 +1 @@
.select-field[data-v-71bc6b38]{width:350px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.select-field[data-v-71bc6b38]{width:100%;margin-bottom:5px}}.active-tag[data-v-693dba04]{color:#409eff;font-weight:700}.active-tag .el-icon-check[data-v-693dba04]{color:#409eff;float:right;margin:7px 0 0 15px}.users-container h1[data-v-693dba04]{margin:22px 0 0 15px}.users-container .pagination[data-v-693dba04]{margin:25px 0;text-align:center}.users-container .search[data-v-693dba04]{width:350px;float:right}.users-container .search-container[data-v-693dba04]{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 15px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.users-container h1[data-v-693dba04]{margin:7px 10px}.users-container .el-dropdown-link[data-v-693dba04]{cursor:pointer;color:#409eff}.users-container .el-icon-arrow-down[data-v-693dba04]{font-size:12px}.users-container .search[data-v-693dba04]{width:100%}.users-container .search-container[data-v-693dba04]{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px 7px}.users-container .el-tag[data-v-693dba04]{width:30px;display:inline-block;margin-bottom:4px;font-weight:700}.users-container .el-tag.el-tag--danger[data-v-693dba04],.users-container .el-tag.el-tag--success[data-v-693dba04]{padding-left:8px}}

View file

@ -0,0 +1 @@
@supports (-webkit-mask:none) and (not (cater-color:#fff)){.login-container .el-input input{color:#fff}.login-container .el-input input:first-line{color:#eee}}.login-container .el-input{display:inline-block;height:47px;width:85%}.login-container .el-input input{background:transparent;border:0;-webkit-appearance:none;border-radius:0;padding:12px 5px 12px 15px;color:#eee;height:47px;caret-color:#fff}.login-container .el-input input:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px #283443 inset!important;-webkit-text-fill-color:#fff!important}.login-container .el-form-item{border:1px solid hsla(0,0%,100%,.1);background:rgba(0,0,0,.1);border-radius:5px;color:#454545}.login-container[data-v-57350b8e]{min-height:100%;width:100%;background-color:#2d3a4b;overflow:hidden}.login-container .login-form[data-v-57350b8e]{position:relative;width:520px;max-width:100%;padding:160px 35px 0;margin:0 auto;overflow:hidden}.login-container .tips[data-v-57350b8e]{font-size:14px;color:#fff;margin-bottom:10px}.login-container .tips span[data-v-57350b8e]:first-of-type{margin-right:16px}.login-container .svg-container[data-v-57350b8e]{padding:6px 5px 6px 15px;color:#889aa4;vertical-align:middle;width:30px;display:inline-block}.login-container .title-container[data-v-57350b8e]{position:relative}.login-container .title-container .title[data-v-57350b8e]{font-size:26px;color:#eee;margin:0 auto 40px;text-align:center;font-weight:700}.login-container .title-container .set-language[data-v-57350b8e]{color:#fff;position:absolute;top:3px;font-size:18px;right:0;cursor:pointer}.login-container .show-pwd[data-v-57350b8e]{position:absolute;right:10px;top:7px;font-size:16px;color:#889aa4;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.login-container .thirdparty-button[data-v-57350b8e]{position:absolute;right:0;bottom:6px}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
.wscn-http404-container[data-v-b8c8aa9a]{-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);position:absolute;top:40%;left:50%}.wscn-http404[data-v-b8c8aa9a]{position:relative;width:1200px;padding:0 50px;overflow:hidden}.wscn-http404 .pic-404[data-v-b8c8aa9a]{position:relative;float:left;width:600px;overflow:hidden}.wscn-http404 .pic-404__parent[data-v-b8c8aa9a]{width:100%}.wscn-http404 .pic-404__child[data-v-b8c8aa9a]{position:absolute}.wscn-http404 .pic-404__child.left[data-v-b8c8aa9a]{width:80px;top:17px;left:220px;opacity:0;-webkit-animation-name:cloudLeft-data-v-b8c8aa9a;animation-name:cloudLeft-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}.wscn-http404 .pic-404__child.mid[data-v-b8c8aa9a]{width:46px;top:10px;left:420px;opacity:0;-webkit-animation-name:cloudMid-data-v-b8c8aa9a;animation-name:cloudMid-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1.2s;animation-delay:1.2s}.wscn-http404 .pic-404__child.right[data-v-b8c8aa9a]{width:62px;top:100px;left:500px;opacity:0;-webkit-animation-name:cloudRight-data-v-b8c8aa9a;animation-name:cloudRight-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}@-webkit-keyframes cloudLeft-data-v-b8c8aa9a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@keyframes cloudLeft-data-v-b8c8aa9a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@-webkit-keyframes cloudMid-data-v-b8c8aa9a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@keyframes cloudMid-data-v-b8c8aa9a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@-webkit-keyframes cloudRight-data-v-b8c8aa9a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}@keyframes cloudRight-data-v-b8c8aa9a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}.wscn-http404 .bullshit[data-v-b8c8aa9a]{position:relative;float:left;width:300px;padding:30px 0;overflow:hidden}.wscn-http404 .bullshit__oops[data-v-b8c8aa9a]{font-size:32px;line-height:40px;color:#1482f0;margin-bottom:20px;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__headline[data-v-b8c8aa9a],.wscn-http404 .bullshit__oops[data-v-b8c8aa9a]{font-weight:700;opacity:0;-webkit-animation-name:slideUp-data-v-b8c8aa9a;animation-name:slideUp-data-v-b8c8aa9a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__headline[data-v-b8c8aa9a]{font-size:20px;line-height:24px;color:#222;margin-bottom:10px;-webkit-animation-delay:.1s;animation-delay:.1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-b8c8aa9a]{font-size:13px;line-height:21px;color:grey;margin-bottom:30px;-webkit-animation-delay:.2s;animation-delay:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-b8c8aa9a],.wscn-http404 .bullshit__return-home[data-v-b8c8aa9a]{opacity:0;-webkit-animation-name:slideUp-data-v-b8c8aa9a;animation-name:slideUp-data-v-b8c8aa9a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__return-home[data-v-b8c8aa9a]{display:block;float:left;width:110px;height:36px;background:#1482f0;border-radius:100px;text-align:center;color:#fff;font-size:14px;line-height:36px;cursor:pointer;-webkit-animation-delay:.3s;animation-delay:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}@-webkit-keyframes slideUp-data-v-b8c8aa9a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes slideUp-data-v-b8c8aa9a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}

View file

@ -0,0 +1 @@
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit;font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}[hidden],template{display:none}#nprogress{pointer-events:none}#nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;-webkit-box-shadow:0 0 10px #29d,0 0 5px #29d;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translateY(-4px);transform:rotate(3deg) translateY(-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;-webkit-box-sizing:border-box;box-sizing:border-box;border-color:#29d transparent transparent #29d;border-style:solid;border-width:2px;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .bar,.nprogress-custom-parent #nprogress .spinner{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View file

@ -0,0 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([["7zzA"],{"7zzA":function(e,r,n){"use strict";n.r(r);var t={beforeCreate:function(){var e=this.$route,r=e.params,n=e.query,t=r.path;this.$router.replace({path:"/"+t,query:n})},render:function(e){return e()}},o=n("KHd+"),u=Object(o.a)(t,void 0,void 0,!1,null,null,null);u.options.__file="index.vue";r.default=u.exports}}]);

View file

@ -0,0 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([["JEtC"],{JEtC:function(o,n,i){"use strict";i.r(n);var e={name:"AuthRedirect",created:function(){var o=window.location.search.slice(1);window.opener.location.href=window.location.origin+"/login#"+o,window.close()}},t=i("KHd+"),c=Object(t.a)(e,void 0,void 0,!1,null,null,null);c.options.__file="authredirect.vue";n.default=c.exports}}]);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-18e1"],{BF41:function(t,a,i){},"UUO+":function(t,a,i){"use strict";i.r(a);var e=i("zGwZ"),s=i.n(e),r={name:"Page401",data:function(){return{errGif:s.a+"?"+ +new Date,ewizardClap:"https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646",dialogVisible:!1}},methods:{back:function(){this.$route.query.noGoBack?this.$router.push({path:"/dashboard"}):this.$router.go(-1)}}},n=(i("UrVv"),i("KHd+")),l=Object(n.a)(r,function(){var t=this,a=t.$createElement,i=t._self._c||a;return i("div",{staticClass:"errPage-container"},[i("el-button",{staticClass:"pan-back-btn",attrs:{icon:"arrow-left"},on:{click:t.back}},[t._v("返回")]),t._v(" "),i("el-row",[i("el-col",{attrs:{span:12}},[i("h1",{staticClass:"text-jumbo text-ginormous"},[t._v("Oops!")]),t._v("\n gif来源"),i("a",{attrs:{href:"https://zh.airbnb.com/",target:"_blank"}},[t._v("airbnb")]),t._v(" 页面\n "),i("h2",[t._v("你没有权限去该页面")]),t._v(" "),i("h6",[t._v("如有不满请联系你领导")]),t._v(" "),i("ul",{staticClass:"list-unstyled"},[i("li",[t._v("或者你可以去:")]),t._v(" "),i("li",{staticClass:"link-type"},[i("router-link",{attrs:{to:"/dashboard"}},[t._v("回首页")])],1),t._v(" "),i("li",{staticClass:"link-type"},[i("a",{attrs:{href:"https://www.taobao.com/"}},[t._v("随便看看")])]),t._v(" "),i("li",[i("a",{attrs:{href:"#"},on:{click:function(a){a.preventDefault(),t.dialogVisible=!0}}},[t._v("点我看图")])])])]),t._v(" "),i("el-col",{attrs:{span:12}},[i("img",{attrs:{src:t.errGif,width:"313",height:"428",alt:"Girl has dropped her ice cream."}})])],1),t._v(" "),i("el-dialog",{attrs:{visible:t.dialogVisible,title:"随便看"},on:{"update:visible":function(a){t.dialogVisible=a}}},[i("img",{staticClass:"pan-img",attrs:{src:t.ewizardClap}})])],1)},[],!1,null,"ab9be52c",null);l.options.__file="401.vue";a.default=l.exports},UrVv:function(t,a,i){"use strict";var e=i("BF41");i.n(e).a},zGwZ:function(t,a,i){t.exports=i.p+"static/img/401.089007e.gif"}}]);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-8b70"],{K3CD:function(e,t,s){},ZvHC:function(e,t,s){"use strict";var n=s("K3CD");s.n(n).a},c11S:function(e,t,s){"use strict";var n=s("gTgX");s.n(n).a},gTgX:function(e,t,s){},ntYl:function(e,t,s){"use strict";s.r(t);var n=s("J4zp"),o=s.n(n),a=s("XJYT"),r=s("wAo7"),i=s("mSNy"),l={name:"Login",components:{"svg-icon":r.a},data:function(){return{loginForm:{username:"",password:""},passwordType:"password",loading:!1,showDialog:!1,redirect:void 0}},watch:{$route:{handler:function(e){this.redirect=e.query&&e.query.redirect},immediate:!0}},methods:{showPwd:function(){"password"===this.passwordType?this.passwordType="":this.passwordType="password"},handleLogin:function(){var e=this;if(this.loading=!0,this.checkUsername()){var t=this.getLoginData();this.$store.dispatch("LoginByUsername",t).then(function(){e.loading=!1,e.$router.push({path:e.redirect||"/users/index"})}).catch(function(){e.loading=!1})}else Object(a.Message)({message:i.a.t("login.errorMessage"),type:"error",duration:7e3}),this.$store.dispatch("addErrorLog",{message:i.a.t("login.errorMessage")}),this.loading=!1},checkUsername:function(){return this.loginForm.username.includes("@")},getLoginData:function(){var e=this.loginForm.username.split("@"),t=o()(e,2),s=t[0],n=t[1];return{username:s.trim(),authHost:n.trim(),password:this.loginForm.password}}}},c=(s("c11S"),s("ZvHC"),s("KHd+")),p=Object(c.a)(l,function(){var e=this,t=e.$createElement,s=e._self._c||t;return s("div",{staticClass:"login-container"},[s("el-form",{ref:"loginForm",staticClass:"login-form",attrs:{model:e.loginForm,"auto-complete":"on","label-position":"left"}},[s("div",{staticClass:"title-container"},[s("h3",{staticClass:"title"},[e._v("\n "+e._s(e.$t("login.title"))+"\n ")])]),e._v(" "),s("el-form-item",{attrs:{prop:"username"}},[s("span",{staticClass:"svg-container"},[s("svg-icon",{attrs:{"icon-class":"user"}})],1),e._v(" "),s("el-input",{attrs:{placeholder:e.$t("login.username"),name:"username",type:"text","auto-complete":"on"},model:{value:e.loginForm.username,callback:function(t){e.$set(e.loginForm,"username",t)},expression:"loginForm.username"}})],1),e._v(" "),s("el-form-item",{attrs:{prop:"password"}},[s("span",{staticClass:"svg-container"},[s("svg-icon",{attrs:{"icon-class":"password"}})],1),e._v(" "),s("el-input",{attrs:{type:e.passwordType,placeholder:e.$t("login.password"),name:"password","auto-complete":"on"},nativeOn:{keyup:function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"enter",13,t.key,"Enter")?null:e.handleLogin(t)}},model:{value:e.loginForm.password,callback:function(t){e.$set(e.loginForm,"password",t)},expression:"loginForm.password"}}),e._v(" "),s("span",{staticClass:"show-pwd",on:{click:e.showPwd}},[s("svg-icon",{attrs:{"icon-class":"password"===e.passwordType?"eye":"eye-open"}})],1)],1),e._v(" "),s("el-button",{staticStyle:{width:"100%","margin-bottom":"30px"},attrs:{loading:e.loading,type:"primary"},nativeOn:{click:function(t){return t.preventDefault(),e.handleLogin(t)}}},[e._v("\n "+e._s(e.$t("login.logIn"))+"\n ")])],1)],1)},[],!1,null,"57350b8e",null);p.options.__file="index.vue";t.default=p.exports}}]);

File diff suppressed because one or more lines are too long

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