Merge branch 'develop' into feature/report-notes
This commit is contained in:
commit
3ecf131511
197 changed files with 3697 additions and 1058 deletions
|
@ -1,3 +1,3 @@
|
|||
[
|
||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/repo/migrations/*.exs"]
|
||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/repo/migrations/*.exs", "priv/scrubbers/*.ex"]
|
||||
]
|
||||
|
|
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- OStatus: Extract RSS functionality
|
||||
- Deprecated `User.Info` embedded schema (fields moved to `User`)
|
||||
- Store status data inside Flag activity
|
||||
- Deprecated (reorganized as `UserRelationship` entity) User fields with user AP IDs (`blocks`, `mutes`, `muted_reblogs`, `muted_notifications`, `subscribers`).
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
|
||||
|
@ -36,9 +37,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Mastodon API: `pleroma.thread_muted` to the Status entity
|
||||
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
|
||||
- Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
|
||||
- Admin API: Render whole status in grouped reports
|
||||
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
|
||||
</details>
|
||||
|
||||
### Added
|
||||
- `:chat_limit` option to limit chat characters.
|
||||
- Refreshing poll results for remote polls
|
||||
- Authentication: Added rate limit for password-authorized actions / login existence checks
|
||||
- Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app.
|
||||
|
@ -46,6 +50,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Mix task to list all users (`mix pleroma.user list`)
|
||||
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
|
||||
- MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
|
||||
- User notification settings: Add `privacy_option` option.
|
||||
- User settings: Add _This account is a_ option.
|
||||
- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
|
||||
|
@ -67,22 +74,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body)
|
||||
- Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays
|
||||
- Pleroma API: `POST /api/v1/pleroma/conversations/read` to mark all conversations as read
|
||||
- ActivityPub: Support `Move` activities
|
||||
- Mastodon API: Add `/api/v1/markers` for managing timeline read markers
|
||||
- Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations`
|
||||
- Configuration: `feed` option for user atom feed.
|
||||
- Pleroma API: Add Emoji reactions
|
||||
- Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
|
||||
- Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
|
||||
- ActivityPub: Configurable `type` field of the actors.
|
||||
- Mastodon API: `/api/v1/accounts/:id` has `source/pleroma/actor_type` field.
|
||||
- Mastodon API: `/api/v1/update_credentials` accepts `actor_type` field.
|
||||
</details>
|
||||
|
||||
### Fixed
|
||||
- Report emails now include functional links to profiles of remote user accounts
|
||||
- Not being able to log in to some third-party apps when logged in to MastoFE
|
||||
- MRF: `Delete` activities being exempt from MRF policies
|
||||
- OTP releases: Not being able to configure OAuth expired token cleanup interval
|
||||
- OTP releases: Not being able to configure HTML sanitization policy
|
||||
- Favorites timeline now ordered by favorite date instead of post date
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
|
||||
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
|
||||
- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
|
||||
- AdminAPI: If some status received reports both in the "new" format and "old" format it was considered reports on two different statuses (in the context of grouped reports)
|
||||
- Admin API: Error when trying to update reports in the "old" format
|
||||
</details>
|
||||
|
||||
|
|
|
@ -225,6 +225,7 @@
|
|||
notify_email: "noreply@example.com",
|
||||
description: "A Pleroma instance, an alternative fediverse server",
|
||||
limit: 5_000,
|
||||
chat_limit: 5_000,
|
||||
remote_limit: 100_000,
|
||||
upload_limit: 16_000_000,
|
||||
avatar_upload_limit: 2_000_000,
|
||||
|
@ -562,7 +563,10 @@
|
|||
base_path: "/oauth",
|
||||
providers: ueberauth_providers
|
||||
|
||||
config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies
|
||||
config :pleroma,
|
||||
:auth,
|
||||
enforce_oauth_admin_scope_usage: false,
|
||||
oauth_consumer_strategies: oauth_consumer_strategies
|
||||
|
||||
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false
|
||||
|
||||
|
|
|
@ -2094,6 +2094,15 @@
|
|||
type: :group,
|
||||
description: "Authentication / authorization settings",
|
||||
children: [
|
||||
%{
|
||||
key: :enforce_oauth_admin_scope_usage,
|
||||
type: :boolean,
|
||||
description:
|
||||
"OAuth admin scope requirement toggle. " <>
|
||||
"If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token " <>
|
||||
"(client app must support admin scopes). If `false` and token doesn't have admin scope(s)," <>
|
||||
"`is_admin` user flag grants access to admin-specific actions."
|
||||
},
|
||||
%{
|
||||
key: :auth_template,
|
||||
type: :string,
|
||||
|
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
Authentication is required and the user must be an admin.
|
||||
|
||||
Configuration options:
|
||||
|
||||
* `[:auth, :enforce_oauth_admin_scope_usage]` — OAuth admin scope requirement toggle.
|
||||
If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes).
|
||||
If `false` and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions.
|
||||
Note that client app needs to explicitly support admin scopes and request them when obtaining auth token.
|
||||
|
||||
## `GET /api/pleroma/admin/users`
|
||||
|
||||
### List users
|
||||
|
|
|
@ -57,6 +57,7 @@ Has these additional fields under the `pleroma` object:
|
|||
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
|
||||
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
|
||||
- `deactivated`: boolean, true when the user is deactivated
|
||||
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
|
||||
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
|
||||
|
||||
### Source
|
||||
|
@ -65,6 +66,8 @@ Has these additional fields under the `pleroma` object:
|
|||
|
||||
- `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown
|
||||
- `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API
|
||||
- `discoverable`: boolean, true when the user allows discovery of the account in search results and other services.
|
||||
- `actor_type`: string, the type of this account.
|
||||
|
||||
## Conversations
|
||||
|
||||
|
@ -91,11 +94,18 @@ Has these additional fields under the `pleroma` object:
|
|||
|
||||
- `is_seen`: true if the notification was read by the user
|
||||
|
||||
### Move Notification
|
||||
|
||||
The `type` value is `move`. Has an additional field:
|
||||
|
||||
- `target`: new account
|
||||
|
||||
## GET `/api/v1/notifications`
|
||||
|
||||
Accepts additional parameters:
|
||||
|
||||
- `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
|
||||
- `with_move`: boolean, when set to `true` will include Move notifications. `false` by default.
|
||||
|
||||
## POST `/api/v1/statuses`
|
||||
|
||||
|
@ -136,7 +146,10 @@ Additional parameters can be added to the JSON body/Form data:
|
|||
- `default_scope` - the scope returned under `privacy` key in Source subentity
|
||||
- `pleroma_settings_store` - Opaque user settings to be saved on the backend.
|
||||
- `skip_thread_containment` - if true, skip filtering out broken threads
|
||||
- `allow_following_move` - if true, allows automatically follow moved following accounts
|
||||
- `pleroma_background_image` - sets the background image of the user.
|
||||
- `discoverable` - if true, discovery of this account in search results and other services is allowed.
|
||||
- `actor_type` - the type of this account.
|
||||
|
||||
### Pleroma Settings Store
|
||||
Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.
|
||||
|
|
|
@ -302,6 +302,7 @@ See [Admin-API](admin_api.md)
|
|||
* `follows`: BOOLEAN field, receives notifications from people the user follows
|
||||
* `remote`: BOOLEAN field, receives notifications from people on remote instances
|
||||
* `local`: BOOLEAN field, receives notifications from people on the local instance
|
||||
* `privacy_option`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification.
|
||||
* Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}`
|
||||
|
||||
## `/api/pleroma/healthcheck`
|
||||
|
|
|
@ -3,17 +3,26 @@
|
|||
!!! danger
|
||||
This is a Work In Progress, not usable just yet.
|
||||
|
||||
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl config` and in case of source installs it's
|
||||
`mix pleroma.config`.
|
||||
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
|
||||
|
||||
## Transfer config from file to DB.
|
||||
|
||||
```sh
|
||||
$PREFIX migrate_to_db
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl config migrate_to_db
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.config migrate_to_db
|
||||
```
|
||||
|
||||
|
||||
## Transfer config from DB to `config/env.exported_from_db.secret.exs`
|
||||
|
||||
```sh
|
||||
$PREFIX migrate_from_db <env>
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl config migrate_from_db <env>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.config migrate_from_db <env>
|
||||
```
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Database maintenance tasks
|
||||
|
||||
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl database` and in case of source installs it's `mix pleroma.database`.
|
||||
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
|
||||
|
||||
!!! danger
|
||||
These mix tasks can take a long time to complete. Many of them were written to address specific database issues that happened because of bugs in migrations or other specific scenarios. Do not run these tasks "just in case" if everything is fine your instance.
|
||||
|
@ -9,8 +9,12 @@ Every command should be ran with a prefix, in case of OTP releases it is `./bin/
|
|||
|
||||
Replaces embedded objects with references to them in the `objects` table. Only needs to be ran once if the instance was created before Pleroma 1.0.5. The reason why this is not a migration is because it could significantly increase the database size after being ran, however after this `VACUUM FULL` will be able to reclaim about 20% (really depends on what is in the database, your mileage may vary) of the db size before the migration.
|
||||
|
||||
```sh
|
||||
$PREFIX remove_embedded_objects [<options>]
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl database remove_embedded_objects [<options>]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.database remove_embedded_objects [<options>]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
@ -20,11 +24,15 @@ $PREFIX remove_embedded_objects [<options>]
|
|||
|
||||
This will prune remote posts older than 90 days (configurable with [`config :pleroma, :instance, remote_post_retention_days`](../../configuration/cheatsheet.md#instance)) from the database, they will be refetched from source when accessed.
|
||||
|
||||
!!! note
|
||||
The disk space will only be reclaimed after `VACUUM FULL`
|
||||
!!! danger
|
||||
The disk space will only be reclaimed after `VACUUM FULL`. You may run out of disk space during the execution of the task or vacuuming if you don't have about 1/3rds of the database size free.
|
||||
|
||||
```sh
|
||||
$PREFIX pleroma.database prune_objects [<options>]
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl database prune_objects [<options>]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.database prune_objects [<options>]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
@ -34,18 +42,30 @@ $PREFIX pleroma.database prune_objects [<options>]
|
|||
|
||||
Can be safely re-run
|
||||
|
||||
```sh
|
||||
$PREFIX bump_all_conversations
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl database bump_all_conversations
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.database bump_all_conversations
|
||||
```
|
||||
|
||||
## Remove duplicated items from following and update followers count for all users
|
||||
|
||||
```sh
|
||||
$PREFIX update_users_following_followers_counts
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl database update_users_following_followers_counts
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.database update_users_following_followers_counts
|
||||
```
|
||||
|
||||
## Fix the pre-existing "likes" collections for all objects
|
||||
|
||||
```sh
|
||||
$PREFIX fix_likes_collections
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl database fix_likes_collections
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.database fix_likes_collections
|
||||
```
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
# Managing digest emails
|
||||
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl digest` and in case of source installs it's `mix pleroma.digest`.
|
||||
|
||||
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
|
||||
|
||||
## Send digest email since given date (user registration date by default) ignoring user activity status.
|
||||
|
||||
```sh
|
||||
$PREFIX test <nickname> [<since_date>]
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl digest test <nickname> [<since_date>]
|
||||
```
|
||||
|
||||
Example:
|
||||
```sh
|
||||
$PREFIX test donaldtheduck 2019-05-20
|
||||
```sh tab="From Source"
|
||||
mix pleroma.digest test <nickname> [<since_date>]
|
||||
```
|
||||
|
||||
|
||||
Example:
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl digest test donaldtheduck 2019-05-20
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.digest test donaldtheduck 2019-05-20
|
||||
```
|
||||
|
||||
|
|
|
@ -1,28 +1,44 @@
|
|||
# Managing emoji packs
|
||||
|
||||
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl emoji` and in case of source installs it's `mix pleroma.emoji`.
|
||||
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
|
||||
|
||||
## Lists emoji packs and metadata specified in the manifest
|
||||
|
||||
```sh
|
||||
$PREFIX ls-packs [<options>]
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl emoji ls-packs [<options>]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.emoji ls-packs [<options>]
|
||||
```
|
||||
|
||||
|
||||
### Options
|
||||
- `-m, --manifest PATH/URL` - path to a custom manifest, it can either be an URL starting with `http`, in that case the manifest will be fetched from that address, or a local path
|
||||
|
||||
## Fetch, verify and install the specified packs from the manifest into `STATIC-DIR/emoji/PACK-NAME`
|
||||
```sh
|
||||
$PREFIX get-packs [<options>] <packs>
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl emoji get-packs [<options>] <packs>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.emoji get-packs [<options>] <packs>
|
||||
```
|
||||
|
||||
### Options
|
||||
- `-m, --manifest PATH/URL` - same as [`ls-packs`](#ls-packs)
|
||||
|
||||
## Create a new manifest entry and a file list from the specified remote pack file
|
||||
```sh
|
||||
$PREFIX gen-pack PACK-URL
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl emoji gen-pack PACK-URL
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.emoji gen-pack PACK-URL
|
||||
```
|
||||
|
||||
Currently, only .zip archives are recognized as remote pack files and packs are therefore assumed to be zip archives. This command is intended to run interactively and will first ask you some basic questions about the pack, then download the remote file and generate an SHA256 checksum for it, then generate an emoji file list for you.
|
||||
|
||||
The manifest entry will either be written to a newly created `index.json` file or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously.
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
Every command should be ran as the `pleroma` user from it's home directory. For example if you are superuser, you would have to wrap the command in `su pleroma -s $SHELL -lc "$COMMAND"`.
|
||||
|
||||
??? note "From source note about `MIX_ENV`"
|
||||
|
||||
The `mix` command should be prefixed with the name of environment your Pleroma server is running in, usually it's `MIX_ENV=prod`
|
|
@ -1,12 +1,17 @@
|
|||
# Managing instance configuration
|
||||
|
||||
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl instance` and in case of source installs it's `mix pleroma.instance`.
|
||||
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
|
||||
|
||||
## Generate a new configuration file
|
||||
```sh
|
||||
$PREFIX gen [<options>]
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl instance gen [<options>]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.instance gen [<options>]
|
||||
```
|
||||
|
||||
|
||||
If any of the options are left unspecified, you will be prompted interactively.
|
||||
|
||||
### Options
|
||||
|
|
|
@ -1,30 +1,33 @@
|
|||
# Managing relays
|
||||
|
||||
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl relay` and in case of source installs it's `mix pleroma.relay`.
|
||||
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
|
||||
|
||||
## Follow a relay
|
||||
```sh
|
||||
$PREFIX follow <relay_url>
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl relay follow <relay_url>
|
||||
```
|
||||
|
||||
Example:
|
||||
```sh
|
||||
$PREFIX follow https://example.org/relay
|
||||
```sh tab="From Source"
|
||||
mix pleroma.relay follow <relay_url>
|
||||
```
|
||||
|
||||
## Unfollow a remote relay
|
||||
|
||||
```sh
|
||||
$PREFIX unfollow <relay_url>
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl relay unfollow <relay_url>
|
||||
```
|
||||
|
||||
Example:
|
||||
```sh
|
||||
$PREFIX unfollow https://example.org/relay
|
||||
```sh tab="From Source"
|
||||
mix pleroma.relay unfollow <relay_url>
|
||||
```
|
||||
|
||||
## List relay subscriptions
|
||||
|
||||
```sh
|
||||
$PREFIX list
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl relay list
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.relay list
|
||||
```
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
# Managing uploads
|
||||
|
||||
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl uploads` and in case of source installs it's `mix pleroma.uploads`.
|
||||
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
|
||||
|
||||
## Migrate uploads from local to remote storage
|
||||
```sh
|
||||
$PREFIX migrate_local <target_uploader> [<options>]
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl uploads migrate_local <target_uploader> [<options>]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.uploads migrate_local <target_uploader> [<options>]
|
||||
```
|
||||
|
||||
### Options
|
||||
- `--delete` - delete local uploads after migrating them to the target uploader
|
||||
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
# Managing users
|
||||
|
||||
Every command should be ran with a prefix, in case of OTP releases it is `./bin/pleroma_ctl user` and in case of source installs it's `mix pleroma.user`.
|
||||
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
|
||||
|
||||
## Create a user
|
||||
```sh
|
||||
$PREFIX new <nickname> <email> [<options>]
|
||||
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user new <email> [<options>]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user new <email> [<options>]
|
||||
```
|
||||
|
||||
|
||||
### Options
|
||||
- `--name <name>` - the user's display name
|
||||
- `--bio <bio>` - the user's bio
|
||||
|
@ -16,84 +22,159 @@ $PREFIX new <nickname> <email> [<options>]
|
|||
- `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions
|
||||
|
||||
## List local users
|
||||
```sh
|
||||
$PREFIX list
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user list
|
||||
```
|
||||
|
||||
## Generate an invite link
|
||||
```sh
|
||||
$PREFIX invite [<options>]
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user list
|
||||
```
|
||||
|
||||
|
||||
## Generate an invite link
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user invite [<options>]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user invite [<options>]
|
||||
```
|
||||
|
||||
|
||||
### Options
|
||||
- `--expires-at DATE` - last day on which token is active (e.g. "2019-04-05")
|
||||
- `--max-use NUMBER` - maximum numbers of token uses
|
||||
|
||||
## List generated invites
|
||||
```sh
|
||||
$PREFIX invites
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user invites
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user invites
|
||||
```
|
||||
|
||||
|
||||
## Revoke invite
|
||||
```sh
|
||||
$PREFIX revoke_invite <token_or_id>
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user revoke_invite <token_or_id>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user revoke_invite <token_or_id>
|
||||
```
|
||||
|
||||
|
||||
## Delete a user
|
||||
```sh
|
||||
$PREFIX rm <nickname>
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user rm <nickname>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user rm <nickname>
|
||||
```
|
||||
|
||||
|
||||
## Delete user's posts and interactions
|
||||
```sh
|
||||
$PREFIX delete_activities <nickname>
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user delete_activities <nickname>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user delete_activities <nickname>
|
||||
```
|
||||
|
||||
|
||||
## Sign user out from all applications (delete user's OAuth tokens and authorizations)
|
||||
```sh
|
||||
$PREFIX sign_out <nickname>
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user sign_out <nickname>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user sign_out <nickname>
|
||||
```
|
||||
|
||||
|
||||
## Deactivate or activate a user
|
||||
```sh
|
||||
$PREFIX toggle_activated <nickname>
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user toggle_activated <nickname>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user toggle_activated <nickname>
|
||||
```
|
||||
|
||||
|
||||
## Unsubscribe local users from a user and deactivate the user
|
||||
```sh
|
||||
$PREFIX unsubscribe NICKNAME
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user unsubscribe NICKNAME
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user unsubscribe NICKNAME
|
||||
```
|
||||
|
||||
|
||||
## Unsubscribe local users from an instance and deactivate all accounts on it
|
||||
```sh
|
||||
$PREFIX unsubscribe_all_from_instance <instance>
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user unsubscribe_all_from_instance <instance>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user unsubscribe_all_from_instance <instance>
|
||||
```
|
||||
|
||||
|
||||
## Create a password reset link for user
|
||||
```sh
|
||||
$PREFIX reset_password <nickname>
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user reset_password <nickname>
|
||||
```
|
||||
|
||||
## Set the value of the given user's settings
|
||||
```sh
|
||||
$PREFIX set <nickname> [<options>]
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user reset_password <nickname>
|
||||
```
|
||||
|
||||
|
||||
## Set the value of the given user's settings
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user set <nickname> [<options>]
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user set <nickname> [<options>]
|
||||
```
|
||||
|
||||
### Options
|
||||
- `--locked`/`--no-locked` - whether the user should be locked
|
||||
- `--moderator`/`--no-moderator` - whether the user should be a moderator
|
||||
- `--admin`/`--no-admin` - whether the user should be an admin
|
||||
|
||||
## Add tags to a user
|
||||
```sh
|
||||
$PREFIX tag <nickname> <tags>
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user tag <nickname> <tags>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user tag <nickname> <tags>
|
||||
```
|
||||
|
||||
|
||||
## Delete tags from a user
|
||||
```sh
|
||||
$PREFIX untag <nickname> <tags>
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user untag <nickname> <tags>
|
||||
```
|
||||
|
||||
## Toggle confirmation status of the user
|
||||
```sh
|
||||
$PREFIX toggle_confirmed <nickname>
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user untag <nickname> <tags>
|
||||
```
|
||||
|
||||
|
||||
## Toggle confirmation status of the user
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user toggle_confirmed <nickname>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user toggle_confirmed <nickname>
|
||||
```
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic
|
|||
* `notify_email`: Email used for notifications.
|
||||
* `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``.
|
||||
* `limit`: Posts character limit (CW/Subject included in the counter).
|
||||
* `chat_limit`: Character limit of the instance chat messages.
|
||||
* `remote_limit`: Hard character limit beyond which remote posts will be dropped.
|
||||
* `upload_limit`: File size limit of uploads (except for avatar, background, banner).
|
||||
* `avatar_upload_limit`: File size limit of user’s profile avatars.
|
||||
|
|
|
@ -52,7 +52,9 @@ def run(["migrate_from_db", env, delete?]) do
|
|||
|> Enum.each(fn config ->
|
||||
IO.write(
|
||||
file,
|
||||
"config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n"
|
||||
"config :#{config.group}, #{config.key}, #{
|
||||
inspect(Config.from_binary(config.value), limit: :infinity)
|
||||
}\r\n\r\n"
|
||||
)
|
||||
|
||||
if delete? do
|
||||
|
|
83
lib/mix/tasks/pleroma/notification_settings.ex
Normal file
83
lib/mix/tasks/pleroma/notification_settings.ex
Normal file
|
@ -0,0 +1,83 @@
|
|||
defmodule Mix.Tasks.Pleroma.NotificationSettings do
|
||||
@shortdoc "Enable&Disable privacy option for push notifications"
|
||||
@moduledoc """
|
||||
Example:
|
||||
|
||||
> mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588" # set false only for parallel588 user
|
||||
> mix pleroma.notification_settings --privacy-option=true # set true for all users
|
||||
|
||||
"""
|
||||
|
||||
use Mix.Task
|
||||
import Mix.Pleroma
|
||||
import Ecto.Query
|
||||
|
||||
def run(args) do
|
||||
start_pleroma()
|
||||
|
||||
{options, _, _} =
|
||||
OptionParser.parse(
|
||||
args,
|
||||
strict: [
|
||||
privacy_option: :boolean,
|
||||
email_users: :string,
|
||||
nickname_users: :string
|
||||
]
|
||||
)
|
||||
|
||||
privacy_option = Keyword.get(options, :privacy_option)
|
||||
|
||||
if not is_nil(privacy_option) do
|
||||
privacy_option
|
||||
|> build_query(options)
|
||||
|> Pleroma.Repo.update_all([])
|
||||
end
|
||||
|
||||
shell_info("Done")
|
||||
end
|
||||
|
||||
defp build_query(privacy_option, options) do
|
||||
query =
|
||||
from(u in Pleroma.User,
|
||||
update: [
|
||||
set: [
|
||||
notification_settings:
|
||||
fragment(
|
||||
"jsonb_set(notification_settings, '{privacy_option}', ?)",
|
||||
^privacy_option
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
user_emails =
|
||||
options
|
||||
|> Keyword.get(:email_users, "")
|
||||
|> String.split(",")
|
||||
|> Enum.map(&String.trim(&1))
|
||||
|> Enum.reject(&(&1 == ""))
|
||||
|
||||
query =
|
||||
if length(user_emails) > 0 do
|
||||
where(query, [u], u.email in ^user_emails)
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
user_nicknames =
|
||||
options
|
||||
|> Keyword.get(:nickname_users, "")
|
||||
|> String.split(",")
|
||||
|> Enum.map(&String.trim(&1))
|
||||
|> Enum.reject(&(&1 == ""))
|
||||
|
||||
query =
|
||||
if length(user_nicknames) > 0 do
|
||||
where(query, [u], u.nickname in ^user_nicknames)
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
query
|
||||
end
|
||||
end
|
|
@ -8,7 +8,6 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
alias Ecto.Changeset
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserInviteToken
|
||||
alias Pleroma.Web.OAuth
|
||||
|
||||
@shortdoc "Manages Pleroma users"
|
||||
@moduledoc File.read!("docs/administration/CLI_tasks/user.md")
|
||||
|
@ -354,8 +353,7 @@ def run(["sign_out", nickname]) do
|
|||
start_pleroma()
|
||||
|
||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||
OAuth.Token.delete_user_tokens(user)
|
||||
OAuth.Authorization.delete_user_authorizations(user)
|
||||
User.global_sign_out(user)
|
||||
|
||||
shell_info("#{nickname} signed out from all apps.")
|
||||
else
|
||||
|
@ -373,9 +371,9 @@ def run(["list"]) do
|
|||
users
|
||||
|> Enum.each(fn user ->
|
||||
shell_info(
|
||||
"#{user.nickname} moderator: #{user.info.is_moderator}, admin: #{user.info.is_admin}, locked: #{
|
||||
user.info.locked
|
||||
}, deactivated: #{user.info.deactivated}"
|
||||
"#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{
|
||||
user.locked
|
||||
}, deactivated: #{user.deactivated}"
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
@ -393,10 +391,7 @@ defp set_moderator(user, value) do
|
|||
end
|
||||
|
||||
defp set_admin(user, value) do
|
||||
{:ok, user} =
|
||||
user
|
||||
|> Changeset.change(%{is_admin: value})
|
||||
|> User.update_and_set_cache()
|
||||
{:ok, user} = User.admin_api_update(user, %{is_admin: value})
|
||||
|
||||
shell_info("Admin status of #{user.nickname}: #{user.is_admin}")
|
||||
user
|
||||
|
|
|
@ -29,7 +29,8 @@ defmodule Pleroma.Activity do
|
|||
"Create" => "mention",
|
||||
"Follow" => "follow",
|
||||
"Announce" => "reblog",
|
||||
"Like" => "favourite"
|
||||
"Like" => "favourite",
|
||||
"Move" => "move"
|
||||
}
|
||||
|
||||
@mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
|
||||
|
@ -253,9 +254,10 @@ def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
|
|||
def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)
|
||||
def normalize(_), do: nil
|
||||
|
||||
def delete_by_ap_id(id) when is_binary(id) do
|
||||
def delete_all_by_object_ap_id(id) when is_binary(id) do
|
||||
id
|
||||
|> Queries.by_object_id()
|
||||
|> Queries.exclude_type("Delete")
|
||||
|> select([u], u)
|
||||
|> Repo.delete_all()
|
||||
|> elem(1)
|
||||
|
@ -267,7 +269,7 @@ def delete_by_ap_id(id) when is_binary(id) do
|
|||
|> purge_web_resp_cache()
|
||||
end
|
||||
|
||||
def delete_by_ap_id(_), do: nil
|
||||
def delete_all_by_object_ap_id(_), do: nil
|
||||
|
||||
defp purge_web_resp_cache(%Activity{} = activity) do
|
||||
%{path: path} = URI.parse(activity.data["id"])
|
||||
|
|
|
@ -64,4 +64,12 @@ def by_type(query \\ Activity, activity_type) do
|
|||
where: fragment("(?)->>'type' = ?", activity.data, ^activity_type)
|
||||
)
|
||||
end
|
||||
|
||||
@spec exclude_type(query, String.t()) :: query
|
||||
def exclude_type(query \\ Activity, activity_type) do
|
||||
from(
|
||||
activity in query,
|
||||
where: fragment("(?)->>'type' != ?", activity.data, ^activity_type)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -86,7 +86,7 @@ defp maybe_fetch(activities, user, search_query) do
|
|||
{:ok, object} <- Fetcher.fetch_object_from_id(search_query),
|
||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
||||
true <- Visibility.visible_for_user?(activity, user) do
|
||||
activities ++ [activity]
|
||||
[activity | activities]
|
||||
else
|
||||
_ -> activities
|
||||
end
|
||||
|
|
|
@ -30,6 +30,7 @@ def user_agent do
|
|||
# See http://elixir-lang.org/docs/stable/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
def start(_type, _args) do
|
||||
Pleroma.HTML.compile_scrubbers()
|
||||
Pleroma.Config.DeprecationWarnings.warn()
|
||||
setup_instrumenters()
|
||||
|
||||
|
@ -147,8 +148,6 @@ defp oauth_cleanup_child(true),
|
|||
|
||||
defp oauth_cleanup_child(_), do: []
|
||||
|
||||
defp chat_child(:test, _), do: []
|
||||
|
||||
defp chat_child(_env, true) do
|
||||
[Pleroma.Web.ChatChannel.ChatChannelState]
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Clippy do
|
||||
@moduledoc false
|
||||
|
||||
# No software is complete until they have a Clippy implementation.
|
||||
# A ballmer peak _may_ be required to change this module.
|
||||
|
||||
|
|
|
@ -65,4 +65,16 @@ def delete(key) do
|
|||
def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], [])
|
||||
|
||||
def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []
|
||||
|
||||
def enforce_oauth_admin_scope_usage?, do: !!get([:auth, :enforce_oauth_admin_scope_usage])
|
||||
|
||||
def oauth_admin_scopes(scopes) when is_list(scopes) do
|
||||
Enum.flat_map(
|
||||
scopes,
|
||||
fn scope ->
|
||||
["admin:#{scope}"] ++
|
||||
if enforce_oauth_admin_scope_usage?(), do: [], else: [scope]
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
13
lib/pleroma/ecto_enums.ex
Normal file
13
lib/pleroma/ecto_enums.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import EctoEnum
|
||||
|
||||
defenum(UserRelationshipTypeEnum,
|
||||
block: 1,
|
||||
mute: 2,
|
||||
reblog_mute: 3,
|
||||
notification_mute: 4,
|
||||
inverse_subscription: 5
|
||||
)
|
|
@ -107,4 +107,26 @@ def following(%User{} = user) do
|
|||
[user.follower_address | following]
|
||||
end
|
||||
end
|
||||
|
||||
def move_following(origin, target) do
|
||||
__MODULE__
|
||||
|> join(:inner, [r], f in assoc(r, :follower))
|
||||
|> where(following_id: ^origin.id)
|
||||
|> where([r, f], f.allow_following_move == true)
|
||||
|> limit(50)
|
||||
|> preload([:follower])
|
||||
|> Repo.all()
|
||||
|> Enum.map(fn following_relationship ->
|
||||
Repo.delete(following_relationship)
|
||||
Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
|
||||
end)
|
||||
|> case do
|
||||
[] ->
|
||||
User.update_follower_count(origin)
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
move_following(origin, target)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,25 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTML do
|
||||
# Scrubbers are compiled on boot so they can be configured in OTP releases
|
||||
# @on_load :compile_scrubbers
|
||||
|
||||
def compile_scrubbers do
|
||||
dir = Path.join(:code.priv_dir(:pleroma), "scrubbers")
|
||||
|
||||
dir
|
||||
|> File.ls!()
|
||||
|> Enum.map(&Path.join(dir, &1))
|
||||
|> Kernel.ParallelCompiler.compile()
|
||||
|> case do
|
||||
{:error, _errors, _warnings} ->
|
||||
raise "Compiling scrubbers failed"
|
||||
|
||||
{:ok, _modules, _warnings} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber]
|
||||
defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers
|
||||
defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default]
|
||||
|
@ -99,215 +118,3 @@ def extract_first_external_url(object, content) do
|
|||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Pleroma.HTML.Scrubber.TwitterText do
|
||||
@moduledoc """
|
||||
An HTML scrubbing policy which limits to twitter-style text. Only
|
||||
paragraphs, breaks and links are allowed through the filter.
|
||||
"""
|
||||
|
||||
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
|
||||
|
||||
require FastSanitize.Sanitizer.Meta
|
||||
alias FastSanitize.Sanitizer.Meta
|
||||
|
||||
Meta.strip_comments()
|
||||
|
||||
# links
|
||||
Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes)
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:a, "class", [
|
||||
"hashtag",
|
||||
"u-url",
|
||||
"mention",
|
||||
"u-url mention",
|
||||
"mention u-url"
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:a, "rel", [
|
||||
"tag",
|
||||
"nofollow",
|
||||
"noopener",
|
||||
"noreferrer"
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:a, ["name", "title"])
|
||||
|
||||
# paragraphs and linebreaks
|
||||
Meta.allow_tag_with_these_attributes(:br, [])
|
||||
Meta.allow_tag_with_these_attributes(:p, [])
|
||||
|
||||
# microformats
|
||||
Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"])
|
||||
Meta.allow_tag_with_these_attributes(:span, [])
|
||||
|
||||
# allow inline images for custom emoji
|
||||
if Pleroma.Config.get([:markup, :allow_inline_images]) do
|
||||
# restrict img tags to http/https only, because of MediaProxy.
|
||||
Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:img, [
|
||||
"width",
|
||||
"height",
|
||||
"class",
|
||||
"title",
|
||||
"alt"
|
||||
])
|
||||
end
|
||||
|
||||
Meta.strip_everything_not_covered()
|
||||
end
|
||||
|
||||
defmodule Pleroma.HTML.Scrubber.Default do
|
||||
@doc "The default HTML scrubbing policy: no "
|
||||
|
||||
require FastSanitize.Sanitizer.Meta
|
||||
alias FastSanitize.Sanitizer.Meta
|
||||
# credo:disable-for-previous-line
|
||||
# No idea how to fix this one…
|
||||
|
||||
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
|
||||
|
||||
Meta.strip_comments()
|
||||
|
||||
Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes)
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:a, "class", [
|
||||
"hashtag",
|
||||
"u-url",
|
||||
"mention",
|
||||
"u-url mention",
|
||||
"mention u-url"
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:a, "rel", [
|
||||
"tag",
|
||||
"nofollow",
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"ugc"
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:a, ["name", "title"])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:abbr, ["title"])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:b, [])
|
||||
Meta.allow_tag_with_these_attributes(:blockquote, [])
|
||||
Meta.allow_tag_with_these_attributes(:br, [])
|
||||
Meta.allow_tag_with_these_attributes(:code, [])
|
||||
Meta.allow_tag_with_these_attributes(:del, [])
|
||||
Meta.allow_tag_with_these_attributes(:em, [])
|
||||
Meta.allow_tag_with_these_attributes(:i, [])
|
||||
Meta.allow_tag_with_these_attributes(:li, [])
|
||||
Meta.allow_tag_with_these_attributes(:ol, [])
|
||||
Meta.allow_tag_with_these_attributes(:p, [])
|
||||
Meta.allow_tag_with_these_attributes(:pre, [])
|
||||
Meta.allow_tag_with_these_attributes(:strong, [])
|
||||
Meta.allow_tag_with_these_attributes(:sub, [])
|
||||
Meta.allow_tag_with_these_attributes(:sup, [])
|
||||
Meta.allow_tag_with_these_attributes(:u, [])
|
||||
Meta.allow_tag_with_these_attributes(:ul, [])
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"])
|
||||
Meta.allow_tag_with_these_attributes(:span, [])
|
||||
|
||||
@allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images])
|
||||
|
||||
if @allow_inline_images do
|
||||
# restrict img tags to http/https only, because of MediaProxy.
|
||||
Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:img, [
|
||||
"width",
|
||||
"height",
|
||||
"class",
|
||||
"title",
|
||||
"alt"
|
||||
])
|
||||
end
|
||||
|
||||
if Pleroma.Config.get([:markup, :allow_tables]) do
|
||||
Meta.allow_tag_with_these_attributes(:table, [])
|
||||
Meta.allow_tag_with_these_attributes(:tbody, [])
|
||||
Meta.allow_tag_with_these_attributes(:td, [])
|
||||
Meta.allow_tag_with_these_attributes(:th, [])
|
||||
Meta.allow_tag_with_these_attributes(:thead, [])
|
||||
Meta.allow_tag_with_these_attributes(:tr, [])
|
||||
end
|
||||
|
||||
if Pleroma.Config.get([:markup, :allow_headings]) do
|
||||
Meta.allow_tag_with_these_attributes(:h1, [])
|
||||
Meta.allow_tag_with_these_attributes(:h2, [])
|
||||
Meta.allow_tag_with_these_attributes(:h3, [])
|
||||
Meta.allow_tag_with_these_attributes(:h4, [])
|
||||
Meta.allow_tag_with_these_attributes(:h5, [])
|
||||
end
|
||||
|
||||
if Pleroma.Config.get([:markup, :allow_fonts]) do
|
||||
Meta.allow_tag_with_these_attributes(:font, ["face"])
|
||||
end
|
||||
|
||||
Meta.strip_everything_not_covered()
|
||||
end
|
||||
|
||||
defmodule Pleroma.HTML.Transform.MediaProxy do
|
||||
@moduledoc "Transforms inline image URIs to use MediaProxy."
|
||||
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
def before_scrub(html), do: html
|
||||
|
||||
def scrub_attribute(:img, {"src", "http" <> target}) do
|
||||
media_url =
|
||||
("http" <> target)
|
||||
|> MediaProxy.url()
|
||||
|
||||
{"src", media_url}
|
||||
end
|
||||
|
||||
def scrub_attribute(_tag, attribute), do: attribute
|
||||
|
||||
def scrub({:img, attributes, children}) do
|
||||
attributes =
|
||||
attributes
|
||||
|> Enum.map(fn attr -> scrub_attribute(:img, attr) end)
|
||||
|> Enum.reject(&is_nil(&1))
|
||||
|
||||
{:img, attributes, children}
|
||||
end
|
||||
|
||||
def scrub({:comment, _text, _children}), do: ""
|
||||
|
||||
def scrub({tag, attributes, children}), do: {tag, attributes, children}
|
||||
def scrub({_tag, children}), do: children
|
||||
def scrub(text), do: text
|
||||
end
|
||||
|
||||
defmodule Pleroma.HTML.Scrubber.LinksOnly do
|
||||
@moduledoc """
|
||||
An HTML scrubbing policy which limits to links only.
|
||||
"""
|
||||
|
||||
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
|
||||
|
||||
require FastSanitize.Sanitizer.Meta
|
||||
alias FastSanitize.Sanitizer.Meta
|
||||
|
||||
Meta.strip_comments()
|
||||
|
||||
# links
|
||||
Meta.allow_tag_with_uri_attributes(:a, ["href"], @valid_schemes)
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:a, "rel", [
|
||||
"tag",
|
||||
"nofollow",
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"me",
|
||||
"ugc"
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:a, ["name", "title"])
|
||||
Meta.strip_everything_not_covered()
|
||||
end
|
||||
|
|
|
@ -21,6 +21,8 @@ defmodule Pleroma.Notification do
|
|||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@include_muted_option :with_muted
|
||||
|
||||
schema "notifications" do
|
||||
field(:seen, :boolean, default: false)
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
|
@ -34,7 +36,25 @@ def changeset(%Notification{} = notification, attrs) do
|
|||
|> cast(attrs, [:seen])
|
||||
end
|
||||
|
||||
def for_user_query(user, opts \\ []) do
|
||||
defp for_user_query_ap_id_opts(user, opts) do
|
||||
ap_id_relations =
|
||||
[:block] ++
|
||||
if opts[@include_muted_option], do: [], else: [:notification_mute]
|
||||
|
||||
preloaded_ap_ids = User.outgoing_relations_ap_ids(user, ap_id_relations)
|
||||
|
||||
exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
|
||||
|
||||
exclude_notification_muted_opts =
|
||||
Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)
|
||||
|
||||
{exclude_blocked_opts, exclude_notification_muted_opts}
|
||||
end
|
||||
|
||||
def for_user_query(user, opts \\ %{}) do
|
||||
{exclude_blocked_opts, exclude_notification_muted_opts} =
|
||||
for_user_query_ap_id_opts(user, opts)
|
||||
|
||||
Notification
|
||||
|> where(user_id: ^user.id)
|
||||
|> where(
|
||||
|
@ -54,43 +74,75 @@ def for_user_query(user, opts \\ []) do
|
|||
)
|
||||
)
|
||||
|> preload([n, a, o], activity: {a, object: o})
|
||||
|> exclude_muted(user, opts)
|
||||
|> exclude_blocked(user)
|
||||
|> exclude_notification_muted(user, exclude_notification_muted_opts)
|
||||
|> exclude_blocked(user, exclude_blocked_opts)
|
||||
|> exclude_visibility(opts)
|
||||
|> exclude_move(opts)
|
||||
end
|
||||
|
||||
defp exclude_blocked(query, user) do
|
||||
defp exclude_blocked(query, user, opts) do
|
||||
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
|
||||
|
||||
query
|
||||
|> where([n, a], a.actor not in ^user.blocks)
|
||||
|> where([n, a], a.actor not in ^blocked_ap_ids)
|
||||
|> where(
|
||||
[n, a],
|
||||
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
|
||||
)
|
||||
end
|
||||
|
||||
defp exclude_muted(query, _, %{with_muted: true}) do
|
||||
defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
|
||||
query
|
||||
end
|
||||
|
||||
defp exclude_muted(query, user, _opts) do
|
||||
defp exclude_notification_muted(query, user, opts) do
|
||||
notification_muted_ap_ids =
|
||||
opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
|
||||
|
||||
query
|
||||
|> where([n, a], a.actor not in ^user.muted_notifications)
|
||||
|> where([n, a], a.actor not in ^notification_muted_ap_ids)
|
||||
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
|
||||
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
|
||||
)
|
||||
|> where([n, a, o, tm], is_nil(tm.user_id))
|
||||
end
|
||||
|
||||
defp exclude_move(query, %{with_move: true}) do
|
||||
query
|
||||
end
|
||||
|
||||
defp exclude_move(query, _opts) do
|
||||
where(query, [n, a], fragment("?->>'type' != 'Move'", a.data))
|
||||
end
|
||||
|
||||
@valid_visibilities ~w[direct unlisted public private]
|
||||
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
when is_list(visibility) do
|
||||
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
|
||||
query
|
||||
|> join(:left, [n, a], mutated_activity in Pleroma.Activity,
|
||||
on:
|
||||
fragment("?->>'context'", a.data) ==
|
||||
fragment("?->>'context'", mutated_activity.data) and
|
||||
fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
|
||||
fragment("?->>'type'", mutated_activity.data) == "Create",
|
||||
as: :mutated_activity
|
||||
)
|
||||
|> where(
|
||||
[n, a],
|
||||
[n, a, mutated_activity: mutated_activity],
|
||||
not fragment(
|
||||
"activity_visibility(?, ?, ?) = ANY (?)",
|
||||
"""
|
||||
CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
|
||||
THEN (activity_visibility(?, ?, ?) = ANY (?))
|
||||
ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
|
||||
""",
|
||||
a.data,
|
||||
a.data,
|
||||
mutated_activity.actor,
|
||||
mutated_activity.recipients,
|
||||
mutated_activity.data,
|
||||
^visibility,
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
|
@ -105,17 +157,7 @@ defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
|||
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
when visibility in @valid_visibilities do
|
||||
query
|
||||
|> where(
|
||||
[n, a],
|
||||
not fragment(
|
||||
"activity_visibility(?, ?, ?) = (?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
)
|
||||
)
|
||||
exclude_visibility(query, [visibility])
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
|
@ -251,10 +293,13 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act
|
|||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
|
||||
when type in ["Like", "Announce", "Follow"] do
|
||||
users = get_notified_from_activity(activity)
|
||||
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
|
||||
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
||||
when type in ["Like", "Announce", "Follow", "Move"] do
|
||||
notifications =
|
||||
activity
|
||||
|> get_notified_from_activity()
|
||||
|> Enum.map(&create_notification(activity, &1))
|
||||
|
||||
{:ok, notifications}
|
||||
end
|
||||
|
||||
|
@ -276,19 +321,15 @@ def create_notification(%Activity{} = activity, %User{} = user) do
|
|||
|
||||
def get_notified_from_activity(activity, local_only \\ true)
|
||||
|
||||
def get_notified_from_activity(
|
||||
%Activity{data: %{"to" => _, "type" => type} = _data} = activity,
|
||||
local_only
|
||||
)
|
||||
when type in ["Create", "Like", "Announce", "Follow"] do
|
||||
recipients =
|
||||
[]
|
||||
|> Utils.maybe_notify_to_recipients(activity)
|
||||
|> Utils.maybe_notify_mentioned_recipients(activity)
|
||||
|> Utils.maybe_notify_subscribers(activity)
|
||||
|> Enum.uniq()
|
||||
|
||||
User.get_users_from_set(recipients, local_only)
|
||||
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
|
||||
when type in ["Create", "Like", "Announce", "Follow", "Move"] do
|
||||
[]
|
||||
|> Utils.maybe_notify_to_recipients(activity)
|
||||
|> Utils.maybe_notify_mentioned_recipients(activity)
|
||||
|> Utils.maybe_notify_subscribers(activity)
|
||||
|> Utils.maybe_notify_followers(activity)
|
||||
|> Enum.uniq()
|
||||
|> User.get_users_from_set(local_only)
|
||||
end
|
||||
|
||||
def get_notified_from_activity(_, _local_only), do: []
|
||||
|
@ -314,7 +355,7 @@ def skip?(:self, activity, user) do
|
|||
def skip?(
|
||||
:followers,
|
||||
activity,
|
||||
%{notification_settings: %{"followers" => false}} = user
|
||||
%{notification_settings: %{followers: false}} = user
|
||||
) do
|
||||
actor = activity.data["actor"]
|
||||
follower = User.get_cached_by_ap_id(actor)
|
||||
|
@ -324,14 +365,14 @@ def skip?(
|
|||
def skip?(
|
||||
:non_followers,
|
||||
activity,
|
||||
%{notification_settings: %{"non_followers" => false}} = user
|
||||
%{notification_settings: %{non_followers: false}} = user
|
||||
) do
|
||||
actor = activity.data["actor"]
|
||||
follower = User.get_cached_by_ap_id(actor)
|
||||
!User.following?(follower, user)
|
||||
end
|
||||
|
||||
def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = user) do
|
||||
def skip?(:follows, activity, %{notification_settings: %{follows: false}} = user) do
|
||||
actor = activity.data["actor"]
|
||||
followed = User.get_cached_by_ap_id(actor)
|
||||
User.following?(user, followed)
|
||||
|
@ -340,7 +381,7 @@ def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} =
|
|||
def skip?(
|
||||
:non_follows,
|
||||
activity,
|
||||
%{notification_settings: %{"non_follows" => false}} = user
|
||||
%{notification_settings: %{non_follows: false}} = user
|
||||
) do
|
||||
actor = activity.data["actor"]
|
||||
followed = User.get_cached_by_ap_id(actor)
|
||||
|
|
|
@ -23,6 +23,23 @@ defmodule Pleroma.Object do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
def with_joined_activity(query, activity_type \\ "Create", join_type \\ :inner) do
|
||||
object_position = Map.get(query.aliases, :object, 0)
|
||||
|
||||
join(query, join_type, [{object, object_position}], a in Activity,
|
||||
on:
|
||||
fragment(
|
||||
"COALESCE(?->'object'->>'id', ?->>'object') = (? ->> 'id') AND (?->>'type' = ?) ",
|
||||
a.data,
|
||||
a.data,
|
||||
object.data,
|
||||
a.data,
|
||||
^activity_type
|
||||
),
|
||||
as: :object_activity
|
||||
)
|
||||
end
|
||||
|
||||
def create(data) do
|
||||
Object.change(%Object{}, %{data: data})
|
||||
|> Repo.insert()
|
||||
|
@ -147,7 +164,7 @@ def swap_object_with_tombstone(object) do
|
|||
|
||||
def delete(%Object{data: %{"id" => id}} = object) do
|
||||
with {:ok, _obj} = swap_object_with_tombstone(object),
|
||||
deleted_activity = Activity.delete_by_ap_id(id),
|
||||
deleted_activity = Activity.delete_all_by_object_ap_id(id),
|
||||
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
|
||||
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
|
||||
{:ok, object, deleted_activity}
|
||||
|
|
|
@ -13,28 +13,31 @@ defmodule Pleroma.Pagination do
|
|||
alias Pleroma.Repo
|
||||
|
||||
@default_limit 20
|
||||
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
|
||||
|
||||
def fetch_paginated(query, params, type \\ :keyset)
|
||||
def page_keys, do: @page_keys
|
||||
|
||||
def fetch_paginated(query, %{"total" => true} = params, :keyset) do
|
||||
def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil)
|
||||
|
||||
def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do
|
||||
total = Repo.aggregate(query, :count, :id)
|
||||
|
||||
%{
|
||||
total: total,
|
||||
items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset)
|
||||
items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset, table_binding)
|
||||
}
|
||||
end
|
||||
|
||||
def fetch_paginated(query, params, :keyset) do
|
||||
def fetch_paginated(query, params, :keyset, table_binding) do
|
||||
options = cast_params(params)
|
||||
|
||||
query
|
||||
|> paginate(options, :keyset)
|
||||
|> paginate(options, :keyset, table_binding)
|
||||
|> Repo.all()
|
||||
|> enforce_order(options)
|
||||
end
|
||||
|
||||
def fetch_paginated(query, %{"total" => true} = params, :offset) do
|
||||
def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do
|
||||
total =
|
||||
query
|
||||
|> Ecto.Query.exclude(:left_join)
|
||||
|
@ -42,34 +45,34 @@ def fetch_paginated(query, %{"total" => true} = params, :offset) do
|
|||
|
||||
%{
|
||||
total: total,
|
||||
items: fetch_paginated(query, Map.drop(params, ["total"]), :offset)
|
||||
items: fetch_paginated(query, Map.drop(params, ["total"]), :offset, table_binding)
|
||||
}
|
||||
end
|
||||
|
||||
def fetch_paginated(query, params, :offset) do
|
||||
def fetch_paginated(query, params, :offset, table_binding) do
|
||||
options = cast_params(params)
|
||||
|
||||
query
|
||||
|> paginate(options, :offset)
|
||||
|> paginate(options, :offset, table_binding)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def paginate(query, options, method \\ :keyset)
|
||||
def paginate(query, options, method \\ :keyset, table_binding \\ nil)
|
||||
|
||||
def paginate(query, options, :keyset) do
|
||||
def paginate(query, options, :keyset, table_binding) do
|
||||
query
|
||||
|> restrict(:min_id, options)
|
||||
|> restrict(:since_id, options)
|
||||
|> restrict(:max_id, options)
|
||||
|> restrict(:order, options)
|
||||
|> restrict(:limit, options)
|
||||
|> restrict(:min_id, options, table_binding)
|
||||
|> restrict(:since_id, options, table_binding)
|
||||
|> restrict(:max_id, options, table_binding)
|
||||
|> restrict(:order, options, table_binding)
|
||||
|> restrict(:limit, options, table_binding)
|
||||
end
|
||||
|
||||
def paginate(query, options, :offset) do
|
||||
def paginate(query, options, :offset, table_binding) do
|
||||
query
|
||||
|> restrict(:order, options)
|
||||
|> restrict(:offset, options)
|
||||
|> restrict(:limit, options)
|
||||
|> restrict(:order, options, table_binding)
|
||||
|> restrict(:offset, options, table_binding)
|
||||
|> restrict(:limit, options, table_binding)
|
||||
end
|
||||
|
||||
defp cast_params(params) do
|
||||
|
@ -78,7 +81,8 @@ defp cast_params(params) do
|
|||
since_id: :string,
|
||||
max_id: :string,
|
||||
offset: :integer,
|
||||
limit: :integer
|
||||
limit: :integer,
|
||||
skip_order: :boolean
|
||||
}
|
||||
|
||||
params =
|
||||
|
@ -91,38 +95,48 @@ defp cast_params(params) do
|
|||
changeset.changes
|
||||
end
|
||||
|
||||
defp restrict(query, :min_id, %{min_id: min_id}) do
|
||||
where(query, [q], q.id > ^min_id)
|
||||
defp restrict(query, :min_id, %{min_id: min_id}, table_binding) do
|
||||
where(query, [{q, table_position(query, table_binding)}], q.id > ^min_id)
|
||||
end
|
||||
|
||||
defp restrict(query, :since_id, %{since_id: since_id}) do
|
||||
where(query, [q], q.id > ^since_id)
|
||||
defp restrict(query, :since_id, %{since_id: since_id}, table_binding) do
|
||||
where(query, [{q, table_position(query, table_binding)}], q.id > ^since_id)
|
||||
end
|
||||
|
||||
defp restrict(query, :max_id, %{max_id: max_id}) do
|
||||
where(query, [q], q.id < ^max_id)
|
||||
defp restrict(query, :max_id, %{max_id: max_id}, table_binding) do
|
||||
where(query, [{q, table_position(query, table_binding)}], q.id < ^max_id)
|
||||
end
|
||||
|
||||
defp restrict(query, :order, %{min_id: _}) do
|
||||
order_by(query, [u], fragment("? asc nulls last", u.id))
|
||||
defp restrict(query, :order, %{skip_order: true}, _), do: query
|
||||
|
||||
defp restrict(query, :order, %{min_id: _}, table_binding) do
|
||||
order_by(
|
||||
query,
|
||||
[{u, table_position(query, table_binding)}],
|
||||
fragment("? asc nulls last", u.id)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict(query, :order, _options) do
|
||||
order_by(query, [u], fragment("? desc nulls last", u.id))
|
||||
defp restrict(query, :order, _options, table_binding) do
|
||||
order_by(
|
||||
query,
|
||||
[{u, table_position(query, table_binding)}],
|
||||
fragment("? desc nulls last", u.id)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict(query, :offset, %{offset: offset}) do
|
||||
defp restrict(query, :offset, %{offset: offset}, _table_binding) do
|
||||
offset(query, ^offset)
|
||||
end
|
||||
|
||||
defp restrict(query, :limit, options) do
|
||||
defp restrict(query, :limit, options, _table_binding) do
|
||||
limit = Map.get(options, :limit, @default_limit)
|
||||
|
||||
query
|
||||
|> limit(^limit)
|
||||
end
|
||||
|
||||
defp restrict(query, _, _), do: query
|
||||
defp restrict(query, _, _, _), do: query
|
||||
|
||||
defp enforce_order(result, %{min_id: _}) do
|
||||
result
|
||||
|
@ -130,4 +144,10 @@ defp enforce_order(result, %{min_id: _}) do
|
|||
end
|
||||
|
||||
defp enforce_order(result, _), do: result
|
||||
|
||||
defp table_position(%Ecto.Query{} = query, binding_name) do
|
||||
Map.get(query.aliases, binding_name, 0)
|
||||
end
|
||||
|
||||
defp table_position(_, _), do: 0
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
|
|||
import Plug.Conn
|
||||
import Pleroma.Web.Gettext
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
|
||||
|
||||
@behaviour Plug
|
||||
|
@ -15,6 +16,8 @@ def init(%{scopes: _} = options), do: options
|
|||
def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
|
||||
op = options[:op] || :|
|
||||
token = assigns[:token]
|
||||
|
||||
scopes = transform_scopes(scopes, options)
|
||||
matched_scopes = token && filter_descendants(scopes, token.scopes)
|
||||
|
||||
cond do
|
||||
|
@ -60,6 +63,15 @@ def filter_descendants(scopes, supported_scopes) do
|
|||
)
|
||||
end
|
||||
|
||||
@doc "Transforms scopes by applying supported options (e.g. :admin)"
|
||||
def transform_scopes(scopes, options) do
|
||||
if options[:admin] do
|
||||
Config.oauth_admin_scopes(scopes)
|
||||
else
|
||||
scopes
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do
|
||||
if options[:skip_instance_privacy_check] do
|
||||
conn
|
||||
|
|
21
lib/pleroma/plugs/parsers_plug.ex
Normal file
21
lib/pleroma/plugs/parsers_plug.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Plugs.Parsers do
|
||||
@moduledoc "Initializes Plug.Parsers with upload limit set at boot time"
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
def init(_opts) do
|
||||
Plug.Parsers.init(
|
||||
parsers: [:urlencoded, :multipart, :json],
|
||||
pass: ["*/*"],
|
||||
json_decoder: Jason,
|
||||
length: Pleroma.Config.get([:instance, :upload_limit]),
|
||||
body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
|
||||
)
|
||||
end
|
||||
|
||||
defdelegate call(conn, opts), to: Plug.Parsers
|
||||
end
|
|
@ -5,19 +5,38 @@
|
|||
defmodule Pleroma.Plugs.UserIsAdminPlug do
|
||||
import Pleroma.Web.TranslationHelpers
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do
|
||||
conn
|
||||
def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do
|
||||
token = assigns[:token]
|
||||
|
||||
cond do
|
||||
not Pleroma.Config.enforce_oauth_admin_scope_usage?() ->
|
||||
conn
|
||||
|
||||
token && OAuth.Scopes.contains_admin_scopes?(token.scopes) ->
|
||||
# Note: checking for _any_ admin scope presence, not necessarily fitting requested action.
|
||||
# Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements.
|
||||
conn
|
||||
|
||||
true ->
|
||||
fail(conn)
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
fail(conn)
|
||||
end
|
||||
|
||||
defp fail(conn) do
|
||||
conn
|
||||
|> render_error(:forbidden, "User is not admin.")
|
||||
|> halt
|
||||
|> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.User do
|
|||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
import Ecto, only: [assoc: 2]
|
||||
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Ecto.Multi
|
||||
|
@ -21,6 +22,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Repo
|
||||
alias Pleroma.RepoStreamer
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
@ -42,6 +44,32 @@ defmodule Pleroma.User do
|
|||
@strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
|
||||
@extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
|
||||
|
||||
# AP ID user relationships (blocks, mutes etc.)
|
||||
# Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
|
||||
@user_relationships_config [
|
||||
block: [
|
||||
blocker_blocks: :blocked_users,
|
||||
blockee_blocks: :blocker_users
|
||||
],
|
||||
mute: [
|
||||
muter_mutes: :muted_users,
|
||||
mutee_mutes: :muter_users
|
||||
],
|
||||
reblog_mute: [
|
||||
reblog_muter_mutes: :reblog_muted_users,
|
||||
reblog_mutee_mutes: :reblog_muter_users
|
||||
],
|
||||
notification_mute: [
|
||||
notification_muter_mutes: :notification_muted_users,
|
||||
notification_mutee_mutes: :notification_muter_users
|
||||
],
|
||||
# Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
|
||||
inverse_subscription: [
|
||||
subscribee_subscriptions: :subscriber_users,
|
||||
subscriber_subscriptions: :subscribee_users
|
||||
]
|
||||
]
|
||||
|
||||
schema "users" do
|
||||
field(:bio, :string)
|
||||
field(:email, :string)
|
||||
|
@ -61,7 +89,6 @@ defmodule Pleroma.User do
|
|||
field(:tags, {:array, :string}, default: [])
|
||||
field(:last_refreshed_at, :naive_datetime_usec)
|
||||
field(:last_digest_emailed_at, :naive_datetime)
|
||||
|
||||
field(:banner, :map, default: %{})
|
||||
field(:background, :map, default: %{})
|
||||
field(:source_data, :map, default: %{})
|
||||
|
@ -73,12 +100,7 @@ defmodule Pleroma.User do
|
|||
field(:password_reset_pending, :boolean, default: false)
|
||||
field(:confirmation_token, :string, default: nil)
|
||||
field(:default_scope, :string, default: "public")
|
||||
field(:blocks, {:array, :string}, default: [])
|
||||
field(:domain_blocks, {:array, :string}, default: [])
|
||||
field(:mutes, {:array, :string}, default: [])
|
||||
field(:muted_reblogs, {:array, :string}, default: [])
|
||||
field(:muted_notifications, {:array, :string}, default: [])
|
||||
field(:subscribers, {:array, :string}, default: [])
|
||||
field(:deactivated, :boolean, default: false)
|
||||
field(:no_rich_text, :boolean, default: false)
|
||||
field(:ap_enabled, :boolean, default: false)
|
||||
|
@ -103,24 +125,97 @@ defmodule Pleroma.User do
|
|||
field(:raw_fields, {:array, :map}, default: [])
|
||||
field(:discoverable, :boolean, default: false)
|
||||
field(:invisible, :boolean, default: false)
|
||||
field(:allow_following_move, :boolean, default: true)
|
||||
field(:skip_thread_containment, :boolean, default: false)
|
||||
field(:actor_type, :string, default: "Person")
|
||||
field(:also_known_as, {:array, :string}, default: [])
|
||||
|
||||
field(:notification_settings, :map,
|
||||
default: %{
|
||||
"followers" => true,
|
||||
"follows" => true,
|
||||
"non_follows" => true,
|
||||
"non_followers" => true
|
||||
}
|
||||
embeds_one(
|
||||
:notification_settings,
|
||||
Pleroma.User.NotificationSetting,
|
||||
on_replace: :update
|
||||
)
|
||||
|
||||
has_many(:notifications, Notification)
|
||||
has_many(:registrations, Registration)
|
||||
has_many(:deliveries, Delivery)
|
||||
|
||||
has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
|
||||
has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
|
||||
|
||||
for {relationship_type,
|
||||
[
|
||||
{outgoing_relation, outgoing_relation_target},
|
||||
{incoming_relation, incoming_relation_source}
|
||||
]} <- @user_relationships_config do
|
||||
# Definitions of `has_many :blocker_blocks`, `has_many :muter_mutes` etc.
|
||||
has_many(outgoing_relation, UserRelationship,
|
||||
foreign_key: :source_id,
|
||||
where: [relationship_type: relationship_type]
|
||||
)
|
||||
|
||||
# Definitions of `has_many :blockee_blocks`, `has_many :mutee_mutes` etc.
|
||||
has_many(incoming_relation, UserRelationship,
|
||||
foreign_key: :target_id,
|
||||
where: [relationship_type: relationship_type]
|
||||
)
|
||||
|
||||
# Definitions of `has_many :blocked_users`, `has_many :muted_users` etc.
|
||||
has_many(outgoing_relation_target, through: [outgoing_relation, :target])
|
||||
|
||||
# Definitions of `has_many :blocker_users`, `has_many :muter_users` etc.
|
||||
has_many(incoming_relation_source, through: [incoming_relation, :source])
|
||||
end
|
||||
|
||||
# `:blocks` is deprecated (replaced with `blocked_users` relation)
|
||||
field(:blocks, {:array, :string}, default: [])
|
||||
# `:mutes` is deprecated (replaced with `muted_users` relation)
|
||||
field(:mutes, {:array, :string}, default: [])
|
||||
# `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
|
||||
field(:muted_reblogs, {:array, :string}, default: [])
|
||||
# `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
|
||||
field(:muted_notifications, {:array, :string}, default: [])
|
||||
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
|
||||
field(:subscribers, {:array, :string}, default: [])
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
|
||||
@user_relationships_config do
|
||||
# Definitions of `blocked_users_relation/1`, `muted_users_relation/1`, etc.
|
||||
def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
|
||||
target_users_query = assoc(user, unquote(outgoing_relation_target))
|
||||
|
||||
if restrict_deactivated? do
|
||||
restrict_deactivated(target_users_query)
|
||||
else
|
||||
target_users_query
|
||||
end
|
||||
end
|
||||
|
||||
# Definitions of `blocked_users/1`, `muted_users/1`, etc.
|
||||
def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
|
||||
__MODULE__
|
||||
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
|
||||
user,
|
||||
restrict_deactivated?
|
||||
])
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
# Definitions of `blocked_users_ap_ids/1`, `muted_users_ap_ids/1`, etc.
|
||||
def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
|
||||
__MODULE__
|
||||
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
|
||||
user,
|
||||
restrict_deactivated?
|
||||
])
|
||||
|> select([u], u.ap_id)
|
||||
|> Repo.all()
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Returns if the user should be allowed to authenticate"
|
||||
def auth_active?(%User{deactivated: true}), do: false
|
||||
|
||||
|
@ -251,7 +346,9 @@ def remote_user_creation(params) do
|
|||
:fields,
|
||||
:following_count,
|
||||
:discoverable,
|
||||
:invisible
|
||||
:invisible,
|
||||
:actor_type,
|
||||
:also_known_as
|
||||
]
|
||||
)
|
||||
|> validate_required([:name, :ap_id])
|
||||
|
@ -293,13 +390,16 @@ def update_changeset(struct, params \\ %{}) do
|
|||
:hide_followers_count,
|
||||
:hide_follows_count,
|
||||
:hide_favorites,
|
||||
:allow_following_move,
|
||||
:background,
|
||||
:show_role,
|
||||
:skip_thread_containment,
|
||||
:fields,
|
||||
:raw_fields,
|
||||
:pleroma_settings_store,
|
||||
:discoverable
|
||||
:discoverable,
|
||||
:actor_type,
|
||||
:also_known_as
|
||||
]
|
||||
)
|
||||
|> unique_constraint(:nickname)
|
||||
|
@ -337,9 +437,12 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
|
|||
:hide_follows,
|
||||
:fields,
|
||||
:hide_followers,
|
||||
:allow_following_move,
|
||||
:discoverable,
|
||||
:hide_followers_count,
|
||||
:hide_follows_count
|
||||
:hide_follows_count,
|
||||
:actor_type,
|
||||
:also_known_as
|
||||
]
|
||||
)
|
||||
|> unique_constraint(:nickname)
|
||||
|
@ -759,6 +862,13 @@ def get_friends(user, page \\ nil) do
|
|||
|> Repo.all()
|
||||
end
|
||||
|
||||
def get_friends_ap_ids(user) do
|
||||
user
|
||||
|> get_friends_query(nil)
|
||||
|> select([u], u.ap_id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def get_friends_ids(user, page \\ nil) do
|
||||
user
|
||||
|> get_friends_query(page)
|
||||
|
@ -939,34 +1049,45 @@ def get_recipients_from_activity(%Activity{recipients: to}) do
|
|||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
|
||||
def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
|
||||
add_to_mutes(muter, ap_id, notifications?)
|
||||
@spec mute(User.t(), User.t(), boolean()) ::
|
||||
{:ok, list(UserRelationship.t())} | {:error, String.t()}
|
||||
def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
|
||||
add_to_mutes(muter, mutee, notifications?)
|
||||
end
|
||||
|
||||
def unmute(muter, %{ap_id: ap_id}) do
|
||||
remove_from_mutes(muter, ap_id)
|
||||
def unmute(%User{} = muter, %User{} = mutee) do
|
||||
remove_from_mutes(muter, mutee)
|
||||
end
|
||||
|
||||
def subscribe(subscriber, %{ap_id: ap_id}) do
|
||||
with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
|
||||
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
|
||||
def subscribe(%User{} = subscriber, %User{} = target) do
|
||||
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
|
||||
|
||||
if blocks?(subscribed, subscriber) and deny_follow_blocked do
|
||||
{:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
|
||||
else
|
||||
User.add_to_subscribers(subscribed, subscriber.ap_id)
|
||||
end
|
||||
if blocks?(target, subscriber) and deny_follow_blocked do
|
||||
{:error, "Could not subscribe: #{target.nickname} is blocking you"}
|
||||
else
|
||||
# Note: the relationship is inverse: subscriber acts as relationship target
|
||||
UserRelationship.create_inverse_subscription(target, subscriber)
|
||||
end
|
||||
end
|
||||
|
||||
def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
|
||||
def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
|
||||
with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
|
||||
subscribe(subscriber, subscribee)
|
||||
end
|
||||
end
|
||||
|
||||
def unsubscribe(%User{} = unsubscriber, %User{} = target) do
|
||||
# Note: the relationship is inverse: subscriber acts as relationship target
|
||||
UserRelationship.delete_inverse_subscription(target, unsubscriber)
|
||||
end
|
||||
|
||||
def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
|
||||
with %User{} = user <- get_cached_by_ap_id(ap_id) do
|
||||
User.remove_from_subscribers(user, unsubscriber.ap_id)
|
||||
unsubscribe(unsubscriber, user)
|
||||
end
|
||||
end
|
||||
|
||||
def block(blocker, %User{ap_id: ap_id} = blocked) do
|
||||
def block(%User{} = blocker, %User{} = blocked) do
|
||||
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
|
||||
blocker =
|
||||
if following?(blocker, blocked) do
|
||||
|
@ -983,50 +1104,54 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do
|
|||
nil -> blocked
|
||||
end
|
||||
|
||||
blocker =
|
||||
if subscribed_to?(blocked, blocker) do
|
||||
{:ok, blocker} = unsubscribe(blocked, blocker)
|
||||
blocker
|
||||
else
|
||||
blocker
|
||||
end
|
||||
unsubscribe(blocked, blocker)
|
||||
|
||||
if following?(blocked, blocker), do: unfollow(blocked, blocker)
|
||||
|
||||
{:ok, blocker} = update_follower_count(blocker)
|
||||
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
|
||||
add_to_block(blocker, ap_id)
|
||||
add_to_block(blocker, blocked)
|
||||
end
|
||||
|
||||
# helper to handle the block given only an actor's AP id
|
||||
def block(blocker, %{ap_id: ap_id}) do
|
||||
def block(%User{} = blocker, %{ap_id: ap_id}) do
|
||||
block(blocker, get_cached_by_ap_id(ap_id))
|
||||
end
|
||||
|
||||
def unblock(blocker, %{ap_id: ap_id}) do
|
||||
remove_from_block(blocker, ap_id)
|
||||
def unblock(%User{} = blocker, %User{} = blocked) do
|
||||
remove_from_block(blocker, blocked)
|
||||
end
|
||||
|
||||
# helper to handle the block given only an actor's AP id
|
||||
def unblock(%User{} = blocker, %{ap_id: ap_id}) do
|
||||
unblock(blocker, get_cached_by_ap_id(ap_id))
|
||||
end
|
||||
|
||||
def mutes?(nil, _), do: false
|
||||
def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id)
|
||||
def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
|
||||
|
||||
def mutes_user?(%User{} = user, %User{} = target) do
|
||||
UserRelationship.mute_exists?(user, target)
|
||||
end
|
||||
|
||||
@spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
|
||||
def muted_notifications?(nil, _), do: false
|
||||
|
||||
def muted_notifications?(user, %{ap_id: ap_id}),
|
||||
do: Enum.member?(user.muted_notifications, ap_id)
|
||||
|
||||
def blocks?(%User{} = user, %User{} = target) do
|
||||
blocks_ap_id?(user, target) || blocks_domain?(user, target)
|
||||
end
|
||||
def muted_notifications?(%User{} = user, %User{} = target),
|
||||
do: UserRelationship.notification_mute_exists?(user, target)
|
||||
|
||||
def blocks?(nil, _), do: false
|
||||
|
||||
def blocks_ap_id?(%User{} = user, %User{} = target) do
|
||||
Enum.member?(user.blocks, target.ap_id)
|
||||
def blocks?(%User{} = user, %User{} = target) do
|
||||
blocks_user?(user, target) ||
|
||||
(!User.following?(user, target) && blocks_domain?(user, target))
|
||||
end
|
||||
|
||||
def blocks_ap_id?(_, _), do: false
|
||||
def blocks_user?(%User{} = user, %User{} = target) do
|
||||
UserRelationship.block_exists?(user, target)
|
||||
end
|
||||
|
||||
def blocks_user?(_, _), do: false
|
||||
|
||||
def blocks_domain?(%User{} = user, %User{} = target) do
|
||||
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
|
||||
|
@ -1036,28 +1161,41 @@ def blocks_domain?(%User{} = user, %User{} = target) do
|
|||
|
||||
def blocks_domain?(_, _), do: false
|
||||
|
||||
def subscribed_to?(user, %{ap_id: ap_id}) do
|
||||
def subscribed_to?(%User{} = user, %User{} = target) do
|
||||
# Note: the relationship is inverse: subscriber acts as relationship target
|
||||
UserRelationship.inverse_subscription_exists?(target, user)
|
||||
end
|
||||
|
||||
def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
|
||||
with %User{} = target <- get_cached_by_ap_id(ap_id) do
|
||||
Enum.member?(target.subscribers, user.ap_id)
|
||||
subscribed_to?(user, target)
|
||||
end
|
||||
end
|
||||
|
||||
@spec muted_users(User.t()) :: [User.t()]
|
||||
def muted_users(user) do
|
||||
User.Query.build(%{ap_id: user.mutes, deactivated: false})
|
||||
|> Repo.all()
|
||||
end
|
||||
@doc """
|
||||
Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type.
|
||||
E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
|
||||
"""
|
||||
@spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
|
||||
def outgoing_relations_ap_ids(_, []), do: %{}
|
||||
|
||||
@spec blocked_users(User.t()) :: [User.t()]
|
||||
def blocked_users(user) do
|
||||
User.Query.build(%{ap_id: user.blocks, deactivated: false})
|
||||
|> Repo.all()
|
||||
end
|
||||
def outgoing_relations_ap_ids(%User{} = user, relationship_types)
|
||||
when is_list(relationship_types) do
|
||||
db_result =
|
||||
user
|
||||
|> assoc(:outgoing_relationships)
|
||||
|> join(:inner, [user_rel], u in assoc(user_rel, :target))
|
||||
|> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
|
||||
|> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
|
||||
|> group_by([user_rel, u], user_rel.relationship_type)
|
||||
|> Repo.all()
|
||||
|> Enum.into(%{}, fn [k, v] -> {k, v} end)
|
||||
|
||||
@spec subscribers(User.t()) :: [User.t()]
|
||||
def subscribers(user) do
|
||||
User.Query.build(%{ap_id: user.subscribers, deactivated: false})
|
||||
|> Repo.all()
|
||||
Enum.into(
|
||||
relationship_types,
|
||||
%{},
|
||||
fn rel_type -> {rel_type, db_result[rel_type] || []} end
|
||||
)
|
||||
end
|
||||
|
||||
def deactivate_async(user, status \\ true) do
|
||||
|
@ -1092,20 +1230,9 @@ def deactivate(%User{} = user, status) do
|
|||
end
|
||||
|
||||
def update_notification_settings(%User{} = user, settings) do
|
||||
settings =
|
||||
settings
|
||||
|> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
|
||||
|> Map.new()
|
||||
|
||||
notification_settings =
|
||||
user.notification_settings
|
||||
|> Map.merge(settings)
|
||||
|> Map.take(["followers", "follows", "non_follows", "non_followers"])
|
||||
|
||||
params = %{notification_settings: notification_settings}
|
||||
|
||||
user
|
||||
|> cast(params, [:notification_settings])
|
||||
|> cast(%{notification_settings: settings}, [])
|
||||
|> cast_embed(:notification_settings)
|
||||
|> validate_required([:notification_settings])
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
@ -1164,7 +1291,7 @@ def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
|
|||
blocked_identifiers,
|
||||
fn blocked_identifier ->
|
||||
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
|
||||
{:ok, blocker} <- block(blocker, blocked),
|
||||
{:ok, _user_block} <- block(blocker, blocked),
|
||||
{:ok, _} <- ActivityPub.block(blocker, blocked) do
|
||||
blocked
|
||||
else
|
||||
|
@ -1478,7 +1605,7 @@ def all_superusers do
|
|||
end
|
||||
|
||||
def showing_reblogs?(%User{} = user, %User{} = target) do
|
||||
target.ap_id not in user.muted_reblogs
|
||||
not UserRelationship.reblog_mute_exists?(user, target)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -1720,13 +1847,28 @@ defp truncate_field(%{"name" => name, "value" => value}) do
|
|||
end
|
||||
|
||||
def admin_api_update(user, params) do
|
||||
user
|
||||
|> cast(params, [
|
||||
:is_moderator,
|
||||
:is_admin,
|
||||
:show_role
|
||||
])
|
||||
|> update_and_set_cache()
|
||||
changeset =
|
||||
cast(user, params, [
|
||||
:is_moderator,
|
||||
:is_admin,
|
||||
:show_role
|
||||
])
|
||||
|
||||
with {:ok, updated_user} <- update_and_set_cache(changeset) do
|
||||
if user.is_admin && !updated_user.is_admin do
|
||||
# Tokens & authorizations containing any admin scopes must be revoked (revoking all).
|
||||
# This is an extra safety measure (tokens' admin scopes won't be accepted for non-admins).
|
||||
global_sign_out(user)
|
||||
end
|
||||
|
||||
{:ok, updated_user}
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Signs user out of all applications"
|
||||
def global_sign_out(user) do
|
||||
OAuth.Authorization.delete_user_authorizations(user)
|
||||
OAuth.Token.delete_user_tokens(user)
|
||||
end
|
||||
|
||||
def mascot_update(user, url) do
|
||||
|
@ -1801,23 +1943,6 @@ def update_email_notifications(user, settings) do
|
|||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
defp set_subscribers(user, subscribers) do
|
||||
params = %{subscribers: subscribers}
|
||||
|
||||
user
|
||||
|> cast(params, [:subscribers])
|
||||
|> validate_required([:subscribers])
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
def add_to_subscribers(user, subscribed) do
|
||||
set_subscribers(user, Enum.uniq([subscribed | user.subscribers]))
|
||||
end
|
||||
|
||||
def remove_from_subscribers(user, subscribed) do
|
||||
set_subscribers(user, List.delete(user.subscribers, subscribed))
|
||||
end
|
||||
|
||||
defp set_domain_blocks(user, domain_blocks) do
|
||||
params = %{domain_blocks: domain_blocks}
|
||||
|
||||
|
@ -1835,81 +1960,35 @@ def unblock_domain(user, domain_blocked) do
|
|||
set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
|
||||
end
|
||||
|
||||
defp set_blocks(user, blocks) do
|
||||
params = %{blocks: blocks}
|
||||
|
||||
user
|
||||
|> cast(params, [:blocks])
|
||||
|> validate_required([:blocks])
|
||||
|> update_and_set_cache()
|
||||
@spec add_to_block(User.t(), User.t()) ::
|
||||
{:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp add_to_block(%User{} = user, %User{} = blocked) do
|
||||
UserRelationship.create_block(user, blocked)
|
||||
end
|
||||
|
||||
def add_to_block(user, blocked) do
|
||||
set_blocks(user, Enum.uniq([blocked | user.blocks]))
|
||||
@spec add_to_block(User.t(), User.t()) ::
|
||||
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
|
||||
defp remove_from_block(%User{} = user, %User{} = blocked) do
|
||||
UserRelationship.delete_block(user, blocked)
|
||||
end
|
||||
|
||||
def remove_from_block(user, blocked) do
|
||||
set_blocks(user, List.delete(user.blocks, blocked))
|
||||
end
|
||||
|
||||
defp set_mutes(user, mutes) do
|
||||
params = %{mutes: mutes}
|
||||
|
||||
user
|
||||
|> cast(params, [:mutes])
|
||||
|> validate_required([:mutes])
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
def add_to_mutes(user, muted, notifications?) do
|
||||
with {:ok, user} <- set_mutes(user, Enum.uniq([muted | user.mutes])) do
|
||||
set_notification_mutes(
|
||||
user,
|
||||
Enum.uniq([muted | user.muted_notifications]),
|
||||
notifications?
|
||||
)
|
||||
defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
|
||||
with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
|
||||
{:ok, user_notification_mute} <-
|
||||
(notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
|
||||
{:ok, nil} do
|
||||
{:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_mutes(user, muted) do
|
||||
with {:ok, user} <- set_mutes(user, List.delete(user.mutes, muted)) do
|
||||
set_notification_mutes(
|
||||
user,
|
||||
List.delete(user.muted_notifications, muted),
|
||||
true
|
||||
)
|
||||
defp remove_from_mutes(user, %User{} = muted_user) do
|
||||
with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
|
||||
{:ok, user_notification_mute} <-
|
||||
UserRelationship.delete_notification_mute(user, muted_user) do
|
||||
{:ok, [user_mute, user_notification_mute]}
|
||||
end
|
||||
end
|
||||
|
||||
defp set_notification_mutes(user, _muted_notifications, false = _notifications?) do
|
||||
{:ok, user}
|
||||
end
|
||||
|
||||
defp set_notification_mutes(user, muted_notifications, true = _notifications?) do
|
||||
params = %{muted_notifications: muted_notifications}
|
||||
|
||||
user
|
||||
|> cast(params, [:muted_notifications])
|
||||
|> validate_required([:muted_notifications])
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
def add_reblog_mute(user, ap_id) do
|
||||
params = %{muted_reblogs: user.muted_reblogs ++ [ap_id]}
|
||||
|
||||
user
|
||||
|> cast(params, [:muted_reblogs])
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
def remove_reblog_mute(user, ap_id) do
|
||||
params = %{muted_reblogs: List.delete(user.muted_reblogs, ap_id)}
|
||||
|
||||
user
|
||||
|> cast(params, [:muted_reblogs])
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
def set_invisible(user, invisible) do
|
||||
params = %{invisible: invisible}
|
||||
|
||||
|
|
40
lib/pleroma/user/notification_setting.ex
Normal file
40
lib/pleroma/user/notification_setting.ex
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.User.NotificationSetting do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@derive Jason.Encoder
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:followers, :boolean, default: true)
|
||||
field(:follows, :boolean, default: true)
|
||||
field(:non_follows, :boolean, default: true)
|
||||
field(:non_followers, :boolean, default: true)
|
||||
field(:privacy_option, :boolean, default: false)
|
||||
end
|
||||
|
||||
def changeset(schema, params) do
|
||||
schema
|
||||
|> cast(prepare_attrs(params), [
|
||||
:followers,
|
||||
:follows,
|
||||
:non_follows,
|
||||
:non_followers,
|
||||
:privacy_option
|
||||
])
|
||||
end
|
||||
|
||||
defp prepare_attrs(params) do
|
||||
Enum.reduce(params, %{}, fn
|
||||
{k, v}, acc when is_binary(v) ->
|
||||
Map.put(acc, k, String.downcase(v))
|
||||
|
||||
{k, v}, acc ->
|
||||
Map.put(acc, k, v)
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -103,9 +103,13 @@ defp filter_invisible_users(query) do
|
|||
from(q in query, where: q.invisible == false)
|
||||
end
|
||||
|
||||
defp filter_blocked_user(query, %User{blocks: blocks})
|
||||
when length(blocks) > 0 do
|
||||
from(q in query, where: not (q.ap_id in ^blocks))
|
||||
defp filter_blocked_user(query, %User{} = blocker) do
|
||||
query
|
||||
|> join(:left, [u], b in Pleroma.UserRelationship,
|
||||
as: :blocks,
|
||||
on: b.relationship_type == ^:block and b.source_id == ^blocker.id and u.id == b.target_id
|
||||
)
|
||||
|> where([blocks: b], is_nil(b.target_id))
|
||||
end
|
||||
|
||||
defp filter_blocked_user(query, _), do: query
|
||||
|
|
92
lib/pleroma/user_relationship.ex
Normal file
92
lib/pleroma/user_relationship.ex
Normal file
|
@ -0,0 +1,92 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.UserRelationship do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
|
||||
schema "user_relationships" do
|
||||
belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
|
||||
field(:relationship_type, UserRelationshipTypeEnum)
|
||||
|
||||
timestamps(updated_at: false)
|
||||
end
|
||||
|
||||
for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do
|
||||
# Definitions of `create_block/2`, `create_mute/2` etc.
|
||||
def unquote(:"create_#{relationship_type}")(source, target),
|
||||
do: create(unquote(relationship_type), source, target)
|
||||
|
||||
# Definitions of `delete_block/2`, `delete_mute/2` etc.
|
||||
def unquote(:"delete_#{relationship_type}")(source, target),
|
||||
do: delete(unquote(relationship_type), source, target)
|
||||
|
||||
# Definitions of `block_exists?/2`, `mute_exists?/2` etc.
|
||||
def unquote(:"#{relationship_type}_exists?")(source, target),
|
||||
do: exists?(unquote(relationship_type), source, target)
|
||||
end
|
||||
|
||||
def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do
|
||||
user_relationship
|
||||
|> cast(params, [:relationship_type, :source_id, :target_id])
|
||||
|> validate_required([:relationship_type, :source_id, :target_id])
|
||||
|> unique_constraint(:relationship_type,
|
||||
name: :user_relationships_source_id_relationship_type_target_id_index
|
||||
)
|
||||
|> validate_not_self_relationship()
|
||||
end
|
||||
|
||||
def exists?(relationship_type, %User{} = source, %User{} = target) do
|
||||
UserRelationship
|
||||
|> where(relationship_type: ^relationship_type, source_id: ^source.id, target_id: ^target.id)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
def create(relationship_type, %User{} = source, %User{} = target) do
|
||||
%UserRelationship{}
|
||||
|> changeset(%{
|
||||
relationship_type: relationship_type,
|
||||
source_id: source.id,
|
||||
target_id: target.id
|
||||
})
|
||||
|> Repo.insert(
|
||||
on_conflict: :replace_all_except_primary_key,
|
||||
conflict_target: [:source_id, :relationship_type, :target_id]
|
||||
)
|
||||
end
|
||||
|
||||
def delete(relationship_type, %User{} = source, %User{} = target) do
|
||||
attrs = %{relationship_type: relationship_type, source_id: source.id, target_id: target.id}
|
||||
|
||||
case Repo.get_by(UserRelationship, attrs) do
|
||||
%UserRelationship{} = existing_record -> Repo.delete(existing_record)
|
||||
nil -> {:ok, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do
|
||||
changeset
|
||||
|> validate_change(:target_id, fn _, target_id ->
|
||||
if target_id == get_field(changeset, :source_id) do
|
||||
[target_id: "can't be equal to source_id"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
|> validate_change(:source_id, fn _, source_id ->
|
||||
if source_id == get_field(changeset, :target_id) do
|
||||
[source_id: "can't be equal to target_id"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -456,17 +456,18 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options \\ [
|
|||
user = User.get_cached_by_ap_id(actor)
|
||||
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
|
||||
|
||||
with {:ok, object, activity} <- Object.delete(object),
|
||||
with create_activity <- Activity.get_create_by_object_ap_id(id),
|
||||
data <-
|
||||
%{
|
||||
"type" => "Delete",
|
||||
"actor" => actor,
|
||||
"object" => id,
|
||||
"to" => to,
|
||||
"deleted_activity_id" => activity && activity.id
|
||||
"deleted_activity_id" => create_activity && create_activity.id
|
||||
}
|
||||
|> maybe_put("id", activity_id),
|
||||
{:ok, activity} <- insert(data, local, false),
|
||||
{:ok, object, _create_activity} <- Object.delete(object),
|
||||
stream_out_participations(object, user),
|
||||
_ <- decrease_replies_count_if_reply(object),
|
||||
{:ok, _actor} <- decrease_note_count_if_public(user, object),
|
||||
|
@ -541,6 +542,30 @@ def flag(
|
|||
end
|
||||
end
|
||||
|
||||
def move(%User{} = origin, %User{} = target, local \\ true) do
|
||||
params = %{
|
||||
"type" => "Move",
|
||||
"actor" => origin.ap_id,
|
||||
"object" => origin.ap_id,
|
||||
"target" => target.ap_id
|
||||
}
|
||||
|
||||
with true <- origin.ap_id in target.also_known_as,
|
||||
{:ok, activity} <- insert(params, local) do
|
||||
maybe_federate(activity)
|
||||
|
||||
BackgroundWorker.enqueue("move_following", %{
|
||||
"origin_id" => origin.id,
|
||||
"target_id" => target.id
|
||||
})
|
||||
|
||||
{:ok, activity}
|
||||
else
|
||||
false -> {:error, "Target account must have the origin in `alsoKnownAs`"}
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_activities_for_context_query(context, opts) do
|
||||
public = [Pleroma.Constants.as_public()]
|
||||
|
||||
|
@ -724,6 +749,15 @@ def fetch_user_activities(user, reading_user, params \\ %{}) do
|
|||
|> Map.put("whole_db", true)
|
||||
|> Map.put("pinned_activity_ids", user.pinned_activities)
|
||||
|
||||
params =
|
||||
if User.blocks?(reading_user, user) do
|
||||
params
|
||||
else
|
||||
params
|
||||
|> Map.put("blocking_user", reading_user)
|
||||
|> Map.put("muting_user", reading_user)
|
||||
end
|
||||
|
||||
recipients =
|
||||
user_activities_recipients(%{
|
||||
"godmode" => params["godmode"],
|
||||
|
@ -895,7 +929,7 @@ defp restrict_reblogs(query, _), do: query
|
|||
defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
|
||||
|
||||
defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do
|
||||
mutes = user.mutes
|
||||
mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user)
|
||||
|
||||
query =
|
||||
from([activity] in query,
|
||||
|
@ -912,26 +946,42 @@ defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do
|
|||
|
||||
defp restrict_muted(query, _), do: query
|
||||
|
||||
defp restrict_blocked(query, %{"blocking_user" => %User{} = user}) do
|
||||
blocks = user.blocks || []
|
||||
defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
|
||||
blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user)
|
||||
domain_blocks = user.domain_blocks || []
|
||||
|
||||
following_ap_ids = User.get_friends_ap_ids(user)
|
||||
|
||||
query =
|
||||
if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
|
||||
|
||||
from(
|
||||
[activity, object: o] in query,
|
||||
where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
|
||||
where: fragment("not (? && ?)", activity.recipients, ^blocks),
|
||||
where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids),
|
||||
where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids),
|
||||
where:
|
||||
fragment(
|
||||
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
|
||||
activity.data,
|
||||
activity.data,
|
||||
^blocks
|
||||
^blocked_ap_ids
|
||||
),
|
||||
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks),
|
||||
where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks)
|
||||
where:
|
||||
fragment(
|
||||
"(not (split_part(?, '/', 3) = ANY(?))) or ? = ANY(?)",
|
||||
activity.actor,
|
||||
^domain_blocks,
|
||||
activity.actor,
|
||||
^following_ap_ids
|
||||
),
|
||||
where:
|
||||
fragment(
|
||||
"(not (split_part(?->>'actor', '/', 3) = ANY(?))) or (?->>'actor') = ANY(?)",
|
||||
o.data,
|
||||
^domain_blocks,
|
||||
o.data,
|
||||
^following_ap_ids
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -955,8 +1005,8 @@ defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids})
|
|||
|
||||
defp restrict_pinned(query, _), do: query
|
||||
|
||||
defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user}) do
|
||||
muted_reblogs = user.muted_reblogs || []
|
||||
defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do
|
||||
muted_reblogs = opts["reblog_muted_users_ap_ids"] || User.reblog_muted_users_ap_ids(user)
|
||||
|
||||
from(
|
||||
activity in query,
|
||||
|
@ -1044,7 +1094,33 @@ defp maybe_order(query, %{order: :asc}) do
|
|||
|
||||
defp maybe_order(query, _), do: query
|
||||
|
||||
defp fetch_activities_query_ap_ids_ops(opts) do
|
||||
source_user = opts["muting_user"]
|
||||
ap_id_relations = if source_user, do: [:mute, :reblog_mute], else: []
|
||||
|
||||
ap_id_relations =
|
||||
ap_id_relations ++
|
||||
if opts["blocking_user"] && opts["blocking_user"] == source_user do
|
||||
[:block]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
preloaded_ap_ids = User.outgoing_relations_ap_ids(source_user, ap_id_relations)
|
||||
|
||||
restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts)
|
||||
restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts)
|
||||
|
||||
restrict_muted_reblogs_opts =
|
||||
Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts)
|
||||
|
||||
{restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts}
|
||||
end
|
||||
|
||||
def fetch_activities_query(recipients, opts \\ %{}) do
|
||||
{restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} =
|
||||
fetch_activities_query_ap_ids_ops(opts)
|
||||
|
||||
config = %{
|
||||
skip_thread_containment: Config.get([:instance, :skip_thread_containment])
|
||||
}
|
||||
|
@ -1065,15 +1141,15 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
|||
|> restrict_type(opts)
|
||||
|> restrict_state(opts)
|
||||
|> restrict_favorited_by(opts)
|
||||
|> restrict_blocked(opts)
|
||||
|> restrict_muted(opts)
|
||||
|> restrict_blocked(restrict_blocked_opts)
|
||||
|> restrict_muted(restrict_muted_opts)
|
||||
|> restrict_media(opts)
|
||||
|> restrict_visibility(opts)
|
||||
|> restrict_thread_visibility(opts, config)
|
||||
|> restrict_replies(opts)
|
||||
|> restrict_reblogs(opts)
|
||||
|> restrict_pinned(opts)
|
||||
|> restrict_muted_reblogs(opts)
|
||||
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|
||||
|> restrict_instance(opts)
|
||||
|> Activity.restrict_deactivated_users()
|
||||
|> exclude_poll_votes(opts)
|
||||
|
@ -1089,6 +1165,25 @@ def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
|
|||
|> maybe_update_cc(list_memberships, opts["user"])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetch favorites activities of user with order by sort adds to favorites
|
||||
"""
|
||||
@spec fetch_favourites(User.t(), map(), atom()) :: list(Activity.t())
|
||||
def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
|
||||
user.ap_id
|
||||
|> Activity.Queries.by_actor()
|
||||
|> Activity.Queries.by_type("Like")
|
||||
|> Activity.with_joined_object()
|
||||
|> Object.with_joined_activity()
|
||||
|> select([_like, object, activity], %{activity | object: object})
|
||||
|> order_by([like, _, _], desc: like.id)
|
||||
|> Pagination.fetch_paginated(
|
||||
Map.merge(params, %{"skip_order" => true}),
|
||||
pagination,
|
||||
:object_activity
|
||||
)
|
||||
end
|
||||
|
||||
defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
|
||||
when is_list(list_memberships) and length(list_memberships) > 0 do
|
||||
Enum.map(activities, fn
|
||||
|
@ -1165,6 +1260,7 @@ defp object_to_user_data(data) do
|
|||
data = Transmogrifier.maybe_fix_user_object(data)
|
||||
discoverable = data["discoverable"] || false
|
||||
invisible = data["invisible"] || false
|
||||
actor_type = data["type"] || "Person"
|
||||
|
||||
user_data = %{
|
||||
ap_id: data["id"],
|
||||
|
@ -1179,7 +1275,9 @@ defp object_to_user_data(data) do
|
|||
name: data["name"],
|
||||
follower_address: data["followers"],
|
||||
following_address: data["following"],
|
||||
bio: data["summary"]
|
||||
bio: data["summary"],
|
||||
actor_type: actor_type,
|
||||
also_known_as: Map.get(data, "alsoKnownAs", [])
|
||||
}
|
||||
|
||||
# nickname can be nil because of virtual actors
|
||||
|
|
|
@ -387,7 +387,7 @@ def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} =
|
|||
def handle_incoming(%{"id" => nil}, _options), do: :error
|
||||
def handle_incoming(%{"id" => ""}, _options), do: :error
|
||||
# length of https:// = 8, should validate better, but good enough for now.
|
||||
def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
|
||||
def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
|
||||
do: :error
|
||||
|
||||
# TODO: validate those with a Ecto scheme
|
||||
|
@ -669,7 +669,7 @@ def handle_incoming(
|
|||
|
||||
update_data =
|
||||
new_user_data
|
||||
|> Map.take([:avatar, :banner, :bio, :name])
|
||||
|> Map.take([:avatar, :banner, :bio, :name, :also_known_as])
|
||||
|> Map.put(:fields, fields)
|
||||
|> Map.put(:locked, locked)
|
||||
|> Map.put(:invisible, invisible)
|
||||
|
@ -857,6 +857,24 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Move",
|
||||
"actor" => origin_actor,
|
||||
"object" => origin_actor,
|
||||
"target" => target_actor
|
||||
},
|
||||
_options
|
||||
) do
|
||||
with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
|
||||
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
|
||||
true <- origin_actor in target_user.also_known_as do
|
||||
ActivityPub.move(origin_user, target_user, false)
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(_, _), do: :error
|
||||
|
||||
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
|
||||
|
|
|
@ -722,16 +722,22 @@ defp build_flag_object(act) when is_map(act) or is_binary(act) do
|
|||
act when is_binary(act) -> act
|
||||
end
|
||||
|
||||
activity = Activity.get_by_ap_id_with_object(id)
|
||||
actor = User.get_by_ap_id(activity.object.data["actor"])
|
||||
case Activity.get_by_ap_id_with_object(id) do
|
||||
%Activity{} = activity ->
|
||||
%{
|
||||
"type" => "Note",
|
||||
"id" => activity.data["id"],
|
||||
"content" => activity.object.data["content"],
|
||||
"published" => activity.object.data["published"],
|
||||
"actor" =>
|
||||
AccountView.render("show.json", %{
|
||||
user: User.get_by_ap_id(activity.object.data["actor"])
|
||||
})
|
||||
}
|
||||
|
||||
%{
|
||||
"type" => "Note",
|
||||
"id" => activity.data["id"],
|
||||
"content" => activity.object.data["content"],
|
||||
"published" => activity.object.data["published"],
|
||||
"actor" => AccountView.render("show.json", %{user: actor})
|
||||
}
|
||||
_ ->
|
||||
%{"id" => id, "deleted" => true}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_flag_object(_), do: []
|
||||
|
@ -789,7 +795,52 @@ def get_reports(params, page, page_size) do
|
|||
ActivityPub.fetch_activities([], params, :offset)
|
||||
end
|
||||
|
||||
@spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{
|
||||
def parse_report_group(activity) do
|
||||
reports = get_reports_by_status_id(activity["id"])
|
||||
max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
|
||||
actors = Enum.map(reports, & &1.user_actor)
|
||||
[%{data: %{"object" => [account_id | _]}} | _] = reports
|
||||
|
||||
account =
|
||||
AccountView.render("show.json", %{
|
||||
user: User.get_by_ap_id(account_id)
|
||||
})
|
||||
|
||||
status = get_status_data(activity)
|
||||
|
||||
%{
|
||||
date: max_date.data["published"],
|
||||
account: account,
|
||||
status: status,
|
||||
actors: Enum.uniq(actors),
|
||||
reports: reports
|
||||
}
|
||||
end
|
||||
|
||||
defp get_status_data(status) do
|
||||
case status["deleted"] do
|
||||
true ->
|
||||
%{
|
||||
"id" => status["id"],
|
||||
"deleted" => true
|
||||
}
|
||||
|
||||
_ ->
|
||||
Activity.get_by_ap_id(status["id"])
|
||||
end
|
||||
end
|
||||
|
||||
def get_reports_by_status_id(ap_id) do
|
||||
from(a in Activity,
|
||||
where: fragment("(?)->>'type' = 'Flag'", a.data),
|
||||
where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]),
|
||||
or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id])
|
||||
)
|
||||
|> Activity.with_preloaded_user_actor()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec get_reports_grouped_by_status([String.t()]) :: %{
|
||||
required(:groups) => [
|
||||
%{
|
||||
required(:date) => String.t(),
|
||||
|
@ -798,20 +849,15 @@ def get_reports(params, page, page_size) do
|
|||
required(:actors) => [%User{}],
|
||||
required(:reports) => [%Activity{}]
|
||||
}
|
||||
],
|
||||
required(:total) => integer
|
||||
]
|
||||
}
|
||||
def get_reports_grouped_by_status(groups) do
|
||||
def get_reports_grouped_by_status(activity_ids) do
|
||||
parsed_groups =
|
||||
groups
|
||||
|> Enum.map(fn entry ->
|
||||
activity =
|
||||
case Jason.decode(entry.activity) do
|
||||
{:ok, activity} -> activity
|
||||
_ -> build_flag_object(entry.activity)
|
||||
end
|
||||
|
||||
parse_report_group(activity)
|
||||
activity_ids
|
||||
|> Enum.map(fn id ->
|
||||
id
|
||||
|> build_flag_object()
|
||||
|> parse_report_group()
|
||||
end)
|
||||
|
||||
%{
|
||||
|
@ -819,33 +865,6 @@ def get_reports_grouped_by_status(groups) do
|
|||
}
|
||||
end
|
||||
|
||||
def parse_report_group(activity) do
|
||||
reports = get_reports_by_status_id(activity["id"])
|
||||
max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
|
||||
actors = Enum.map(reports, & &1.user_actor)
|
||||
|
||||
%{
|
||||
date: max_date.data["published"],
|
||||
account: activity["actor"],
|
||||
status: %{
|
||||
id: activity["id"],
|
||||
content: activity["content"],
|
||||
published: activity["published"]
|
||||
},
|
||||
actors: Enum.uniq(actors),
|
||||
reports: reports
|
||||
}
|
||||
end
|
||||
|
||||
def get_reports_by_status_id(ap_id) do
|
||||
from(a in Activity,
|
||||
where: fragment("(?)->>'type' = 'Flag'", a.data),
|
||||
where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}])
|
||||
)
|
||||
|> Activity.with_preloaded_user_actor()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec get_reported_activities() :: [
|
||||
%{
|
||||
required(:activity) => String.t(),
|
||||
|
@ -853,17 +872,23 @@ def get_reports_by_status_id(ap_id) do
|
|||
}
|
||||
]
|
||||
def get_reported_activities do
|
||||
from(a in Activity,
|
||||
where: fragment("(?)->>'type' = 'Flag'", a.data),
|
||||
reported_activities_query =
|
||||
from(a in Activity,
|
||||
where: fragment("(?)->>'type' = 'Flag'", a.data),
|
||||
select: %{
|
||||
activity: fragment("jsonb_array_elements((? #- '{object,0}')->'object')", a.data)
|
||||
},
|
||||
group_by: fragment("activity")
|
||||
)
|
||||
|
||||
from(a in subquery(reported_activities_query),
|
||||
distinct: true,
|
||||
select: %{
|
||||
date: fragment("max(?->>'published') date", a.data),
|
||||
activity:
|
||||
fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity", a.data)
|
||||
},
|
||||
group_by: fragment("activity"),
|
||||
order_by: fragment("date DESC")
|
||||
id: fragment("COALESCE(?->>'id'::text, ? #>> '{}')", a.activity, a.activity)
|
||||
}
|
||||
)
|
||||
|> Repo.all()
|
||||
|> Enum.map(& &1.id)
|
||||
end
|
||||
|
||||
def update_report_state(%Activity{} = activity, state)
|
||||
|
|
|
@ -91,7 +91,7 @@ def render("user.json", %{user: user}) do
|
|||
|
||||
%{
|
||||
"id" => user.ap_id,
|
||||
"type" => "Person",
|
||||
"type" => user.actor_type,
|
||||
"following" => "#{user.ap_id}/following",
|
||||
"followers" => "#{user.ap_id}/followers",
|
||||
"inbox" => "#{user.ap_id}/inbox",
|
||||
|
|
|
@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
|
|||
@spec is_public?(Object.t() | Activity.t() | map()) :: boolean()
|
||||
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
|
||||
def is_public?(%Object{data: data}), do: is_public?(data)
|
||||
def is_public?(%Activity{data: %{"type" => "Move"}}), do: true
|
||||
def is_public?(%Activity{data: data}), do: is_public?(data)
|
||||
def is_public?(%{"directMessage" => true}), do: false
|
||||
def is_public?(data), do: Utils.label_in_message?(Pleroma.Constants.as_public(), data)
|
||||
|
|
|
@ -31,13 +31,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:accounts"]}
|
||||
%{scopes: ["read:accounts"], admin: true}
|
||||
when action in [:list_users, :user_show, :right_get, :invites]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:accounts"]}
|
||||
%{scopes: ["write:accounts"], admin: true}
|
||||
when action in [
|
||||
:get_invite_token,
|
||||
:revoke_invite,
|
||||
|
@ -59,35 +59,37 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:reports"]} when action in [:list_reports, :report_show]
|
||||
%{scopes: ["read:reports"], admin: true}
|
||||
when action in [:list_reports, :report_show]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:reports"]}
|
||||
%{scopes: ["write:reports"], admin: true}
|
||||
when action in [:report_update_state, :report_respond]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:statuses"]} when action == :list_user_statuses
|
||||
%{scopes: ["read:statuses"], admin: true}
|
||||
when action == :list_user_statuses
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:statuses"]}
|
||||
%{scopes: ["write:statuses"], admin: true}
|
||||
when action in [:status_update, :status_delete]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read"]}
|
||||
%{scopes: ["read"], admin: true}
|
||||
when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write"]}
|
||||
%{scopes: ["write"], admin: true}
|
||||
when action in [:relay_follow, :relay_unfollow, :config_update]
|
||||
)
|
||||
|
||||
|
@ -650,11 +652,11 @@ def list_reports(conn, params) do
|
|||
end
|
||||
|
||||
def list_grouped_reports(conn, _params) do
|
||||
reports = Utils.get_reported_activities()
|
||||
statuses = Utils.get_reported_activities()
|
||||
|
||||
conn
|
||||
|> put_view(ReportView)
|
||||
|> render("index_grouped.json", Utils.get_reports_grouped_by_status(reports))
|
||||
|> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))
|
||||
end
|
||||
|
||||
def report_show(conn, %{"id" => id}) do
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Web.AdminAPI.ReportView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.AdminAPI.Report
|
||||
|
@ -46,10 +47,16 @@ def render("show.json", %{report: report, user: user, account: account, statuses
|
|||
def render("index_grouped.json", %{groups: groups}) do
|
||||
reports =
|
||||
Enum.map(groups, fn group ->
|
||||
status =
|
||||
case group.status do
|
||||
%Activity{} = activity -> StatusView.render("show.json", %{activity: activity})
|
||||
_ -> group.status
|
||||
end
|
||||
|
||||
%{
|
||||
date: group[:date],
|
||||
account: group[:account],
|
||||
status: group[:status],
|
||||
status: Map.put_new(status, "deleted", false),
|
||||
actors: Enum.map(group[:actors], &merge_account_views/1),
|
||||
reports:
|
||||
group[:reports]
|
||||
|
|
|
@ -20,7 +20,7 @@ def handle_info(:after_join, socket) do
|
|||
def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}} = socket) do
|
||||
text = String.trim(text)
|
||||
|
||||
if String.length(text) > 0 do
|
||||
if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do
|
||||
author = User.get_cached_by_nickname(user_name)
|
||||
author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author)
|
||||
message = ChatChannelState.add_message(%{text: text, author: author})
|
||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
alias Pleroma.Object
|
||||
alias Pleroma.ThreadMute
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
|
@ -32,7 +33,7 @@ def follow(follower, followed) do
|
|||
def unfollow(follower, unfollowed) do
|
||||
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
|
||||
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
|
||||
{:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
|
||||
{:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
|
||||
{:ok, follower}
|
||||
end
|
||||
end
|
||||
|
@ -420,15 +421,11 @@ defp set_visibility(activity, %{"visibility" => visibility}) do
|
|||
|
||||
defp set_visibility(activity, _), do: {:ok, activity}
|
||||
|
||||
def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
|
||||
if ap_id not in user.muted_reblogs do
|
||||
User.add_reblog_mute(user, ap_id)
|
||||
end
|
||||
def hide_reblogs(%User{} = user, %User{} = target) do
|
||||
UserRelationship.create_reblog_mute(user, target)
|
||||
end
|
||||
|
||||
def show_reblogs(user, %{ap_id: ap_id} = _muted) do
|
||||
if ap_id in user.muted_reblogs do
|
||||
User.remove_reblog_mute(user, ap_id)
|
||||
end
|
||||
def show_reblogs(%User{} = user, %User{} = target) do
|
||||
UserRelationship.delete_reblog_mute(user, target)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -451,6 +451,8 @@ def maybe_notify_to_recipients(
|
|||
recipients ++ to
|
||||
end
|
||||
|
||||
def maybe_notify_to_recipients(recipients, _), do: recipients
|
||||
|
||||
def maybe_notify_mentioned_recipients(
|
||||
recipients,
|
||||
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
|
||||
|
@ -492,7 +494,7 @@ def maybe_notify_subscribers(
|
|||
with %User{} = user <- User.get_cached_by_ap_id(actor) do
|
||||
subscriber_ids =
|
||||
user
|
||||
|> User.subscribers()
|
||||
|> User.subscriber_users()
|
||||
|> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|
||||
|> Enum.map(& &1.ap_id)
|
||||
|
||||
|
@ -502,6 +504,17 @@ def maybe_notify_subscribers(
|
|||
|
||||
def maybe_notify_subscribers(recipients, _), do: recipients
|
||||
|
||||
def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do
|
||||
user
|
||||
|> User.get_followers()
|
||||
|> Enum.map(& &1.ap_id)
|
||||
|> Enum.concat(recipients)
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_notify_followers(recipients, _), do: recipients
|
||||
|
||||
def maybe_extract_mentions(%{"tag" => tag}) do
|
||||
tag
|
||||
|> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end)
|
||||
|
|
|
@ -61,14 +61,7 @@ defmodule Pleroma.Web.Endpoint do
|
|||
plug(Plug.RequestId)
|
||||
plug(Plug.Logger)
|
||||
|
||||
plug(
|
||||
Plug.Parsers,
|
||||
parsers: [:urlencoded, :multipart, :json],
|
||||
pass: ["*/*"],
|
||||
json_decoder: Jason,
|
||||
length: Pleroma.Config.get([:instance, :upload_limit]),
|
||||
body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
|
||||
)
|
||||
plug(Pleroma.Plugs.Parsers)
|
||||
|
||||
plug(Plug.MethodOverride)
|
||||
plug(Plug.Head)
|
||||
|
|
|
@ -152,6 +152,7 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
|
|||
:hide_favorites,
|
||||
:show_role,
|
||||
:skip_thread_containment,
|
||||
:allow_following_move,
|
||||
:discoverable
|
||||
]
|
||||
|> Enum.reduce(%{}, fn key, acc ->
|
||||
|
@ -187,6 +188,7 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
|
|||
{:ok, Map.merge(user.pleroma_settings_store, value)}
|
||||
end)
|
||||
|> add_if_present(params, "default_scope", :default_scope)
|
||||
|> add_if_present(params, "actor_type", :actor_type)
|
||||
|
||||
emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
|
||||
|
||||
|
@ -248,7 +250,11 @@ def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
|
|||
@doc "GET /api/v1/accounts/:id/statuses"
|
||||
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
|
||||
params = Map.put(params, "tag", params["tagged"])
|
||||
params =
|
||||
params
|
||||
|> Map.put("tag", params["tagged"])
|
||||
|> Map.delete("godmode")
|
||||
|
||||
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
|
||||
|
||||
conn
|
||||
|
@ -323,7 +329,7 @@ def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) d
|
|||
def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
|
||||
notifications? = params |> Map.get("notifications", true) |> truthy_param?()
|
||||
|
||||
with {:ok, muter} <- User.mute(muter, muted, notifications?) do
|
||||
with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do
|
||||
render(conn, "relationship.json", user: muter, target: muted)
|
||||
else
|
||||
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||
|
@ -332,7 +338,7 @@ def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
|
|||
|
||||
@doc "POST /api/v1/accounts/:id/unmute"
|
||||
def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
|
||||
with {:ok, muter} <- User.unmute(muter, muted) do
|
||||
with {:ok, _user_relationships} <- User.unmute(muter, muted) do
|
||||
render(conn, "relationship.json", user: muter, target: muted)
|
||||
else
|
||||
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||
|
@ -341,7 +347,7 @@ def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
|
|||
|
||||
@doc "POST /api/v1/accounts/:id/block"
|
||||
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
|
||||
with {:ok, blocker} <- User.block(blocker, blocked),
|
||||
with {:ok, _user_block} <- User.block(blocker, blocked),
|
||||
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
|
||||
render(conn, "relationship.json", user: blocker, target: blocked)
|
||||
else
|
||||
|
@ -351,7 +357,7 @@ def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
|
|||
|
||||
@doc "POST /api/v1/accounts/:id/unblock"
|
||||
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
|
||||
with {:ok, blocker} <- User.unblock(blocker, blocked),
|
||||
with {:ok, _user_block} <- User.unblock(blocker, blocked),
|
||||
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
|
||||
render(conn, "relationship.json", user: blocker, target: blocked)
|
||||
else
|
||||
|
@ -373,12 +379,14 @@ def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
|
|||
|
||||
@doc "GET /api/v1/mutes"
|
||||
def mutes(%{assigns: %{user: user}} = conn, _) do
|
||||
render(conn, "index.json", users: User.muted_users(user), for: user, as: :user)
|
||||
users = User.muted_users(user, _restrict_deactivated = true)
|
||||
render(conn, "index.json", users: users, for: user, as: :user)
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/blocks"
|
||||
def blocks(%{assigns: %{user: user}} = conn, _) do
|
||||
render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user)
|
||||
users = User.blocked_users(user, _restrict_deactivated = true)
|
||||
render(conn, "index.json", users: users, for: user, as: :user)
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/endorsements"
|
||||
|
|
|
@ -346,15 +346,11 @@ def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
|||
|
||||
@doc "GET /api/v1/favourites"
|
||||
def favourites(%{assigns: %{user: user}} = conn, params) do
|
||||
params =
|
||||
params
|
||||
|> Map.put("type", "Create")
|
||||
|> Map.put("favorited_by", user.ap_id)
|
||||
|> Map.put("blocking_user", user)
|
||||
|
||||
activities =
|
||||
ActivityPub.fetch_activities([], params)
|
||||
|> Enum.reverse()
|
||||
ActivityPub.fetch_favourites(
|
||||
user,
|
||||
Map.take(params, Pleroma.Pagination.page_keys())
|
||||
)
|
||||
|
||||
conn
|
||||
|> add_link_headers(activities)
|
||||
|
|
|
@ -24,19 +24,16 @@ def follow(follower, followed, params \\ %{}) do
|
|||
|
||||
with {:ok, follower, _followed, _} <- result do
|
||||
options = cast_params(params)
|
||||
|
||||
case reblogs_visibility(options[:reblogs], result) do
|
||||
{:ok, follower} -> {:ok, follower}
|
||||
_ -> {:ok, follower}
|
||||
end
|
||||
set_reblogs_visibility(options[:reblogs], result)
|
||||
{:ok, follower}
|
||||
end
|
||||
end
|
||||
|
||||
defp reblogs_visibility(false, {:ok, follower, followed, _}) do
|
||||
defp set_reblogs_visibility(false, {:ok, follower, followed, _}) do
|
||||
CommonAPI.hide_reblogs(follower, followed)
|
||||
end
|
||||
|
||||
defp reblogs_visibility(_, {:ok, follower, followed, _}) do
|
||||
defp set_reblogs_visibility(_, {:ok, follower, followed, _}) do
|
||||
CommonAPI.show_reblogs(follower, followed)
|
||||
end
|
||||
|
||||
|
@ -73,7 +70,8 @@ defp cast_params(params) do
|
|||
exclude_types: {:array, :string},
|
||||
exclude_visibilities: {:array, :string},
|
||||
reblogs: :boolean,
|
||||
with_muted: :boolean
|
||||
with_muted: :boolean,
|
||||
with_move: :boolean
|
||||
}
|
||||
|
||||
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
|
||||
|
|
|
@ -50,8 +50,8 @@ def render("relationship.json", %{user: %User{} = user, target: %User{} = target
|
|||
id: to_string(target.id),
|
||||
following: User.following?(user, target),
|
||||
followed_by: User.following?(target, user),
|
||||
blocking: User.blocks_ap_id?(user, target),
|
||||
blocked_by: User.blocks_ap_id?(target, user),
|
||||
blocking: User.blocks_user?(user, target),
|
||||
blocked_by: User.blocks_user?(target, user),
|
||||
muting: User.mutes?(user, target),
|
||||
muting_notifications: User.muted_notifications?(user, target),
|
||||
subscribing: User.subscribed_to?(user, target),
|
||||
|
@ -86,7 +86,7 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
0
|
||||
end
|
||||
|
||||
bot = (user.source_data["type"] || "Person") in ["Application", "Service"]
|
||||
bot = user.actor_type in ["Application", "Service"]
|
||||
|
||||
emojis =
|
||||
(user.source_data["tag"] || [])
|
||||
|
@ -137,7 +137,8 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
sensitive: false,
|
||||
fields: user.raw_fields,
|
||||
pleroma: %{
|
||||
discoverable: user.discoverable
|
||||
discoverable: user.discoverable,
|
||||
actor_type: user.actor_type
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -162,6 +163,7 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
|> maybe_put_chat_token(user, opts[:for], opts)
|
||||
|> maybe_put_activation_status(user, opts[:for])
|
||||
|> maybe_put_follow_requests_count(user, opts[:for])
|
||||
|> maybe_put_allow_following_move(user, opts[:for])
|
||||
|> maybe_put_unread_conversation_count(user, opts[:for])
|
||||
end
|
||||
|
||||
|
@ -238,6 +240,12 @@ defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id:
|
|||
|
||||
defp maybe_put_notification_settings(data, _, _), do: data
|
||||
|
||||
defp maybe_put_allow_following_move(data, %User{id: user_id} = user, %User{id: user_id}) do
|
||||
Kernel.put_in(data, [:pleroma, :allow_following_move], user.allow_following_move)
|
||||
end
|
||||
|
||||
defp maybe_put_allow_following_move(data, _, _), do: data
|
||||
|
||||
defp maybe_put_activation_status(data, user, %User{is_admin: true}) do
|
||||
Kernel.put_in(data, [:pleroma, :deactivated], user.deactivated)
|
||||
end
|
||||
|
|
|
@ -37,32 +37,24 @@ def render("show.json", %{
|
|||
}
|
||||
|
||||
case mastodon_type do
|
||||
"mention" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("show.json", %{activity: activity, for: user})
|
||||
})
|
||||
|
||||
"favourite" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
|
||||
})
|
||||
|
||||
"reblog" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
|
||||
})
|
||||
|
||||
"follow" ->
|
||||
response
|
||||
|
||||
_ ->
|
||||
nil
|
||||
"mention" -> put_status(response, activity, user)
|
||||
"favourite" -> put_status(response, parent_activity, user)
|
||||
"reblog" -> put_status(response, parent_activity, user)
|
||||
"move" -> put_target(response, activity, user)
|
||||
"follow" -> response
|
||||
_ -> nil
|
||||
end
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp put_status(response, activity, user) do
|
||||
Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user}))
|
||||
end
|
||||
|
||||
defp put_target(response, activity, user) do
|
||||
target = User.get_cached_by_ap_id(activity.data["target"])
|
||||
Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user}))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -222,7 +222,7 @@ def token_exchange(
|
|||
{:user_active, true} <- {:user_active, !user.deactivated},
|
||||
{:password_reset_pending, false} <-
|
||||
{:password_reset_pending, user.password_reset_pending},
|
||||
{:ok, scopes} <- validate_scopes(app, params),
|
||||
{:ok, scopes} <- validate_scopes(app, params, user),
|
||||
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
|
||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||
json(conn, Token.Response.build(user, token))
|
||||
|
@ -471,7 +471,7 @@ defp do_create_authorization(
|
|||
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
|
||||
%App{} = app <- Repo.get_by(App, client_id: client_id),
|
||||
true <- redirect_uri in String.split(app.redirect_uris),
|
||||
{:ok, scopes} <- validate_scopes(app, auth_attrs),
|
||||
{:ok, scopes} <- validate_scopes(app, auth_attrs, user),
|
||||
{:auth_active, true} <- {:auth_active, User.auth_active?(user)} do
|
||||
Authorization.create_authorization(app, user, scopes)
|
||||
end
|
||||
|
@ -487,12 +487,12 @@ defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :re
|
|||
defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
|
||||
do: put_session(conn, :registration_id, registration_id)
|
||||
|
||||
@spec validate_scopes(App.t(), map()) ::
|
||||
@spec validate_scopes(App.t(), map(), User.t()) ::
|
||||
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
|
||||
defp validate_scopes(app, params) do
|
||||
defp validate_scopes(%App{} = app, params, %User{} = user) do
|
||||
params
|
||||
|> Scopes.fetch_scopes(app.scopes)
|
||||
|> Scopes.validate(app.scopes)
|
||||
|> Scopes.validate(app.scopes, user)
|
||||
end
|
||||
|
||||
def default_redirect_uri(%App{} = app) do
|
||||
|
|
|
@ -7,6 +7,9 @@ defmodule Pleroma.Web.OAuth.Scopes do
|
|||
Functions for dealing with scopes.
|
||||
"""
|
||||
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.User
|
||||
|
||||
@doc """
|
||||
Fetch scopes from request params.
|
||||
|
||||
|
@ -53,15 +56,36 @@ def to_string(scopes), do: Enum.join(scopes, " ")
|
|||
@doc """
|
||||
Validates scopes.
|
||||
"""
|
||||
@spec validate(list() | nil, list()) ::
|
||||
@spec validate(list() | nil, list(), User.t()) ::
|
||||
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
|
||||
def validate([], _app_scopes), do: {:error, :missing_scopes}
|
||||
def validate(nil, _app_scopes), do: {:error, :missing_scopes}
|
||||
def validate(blank_scopes, _app_scopes, _user) when blank_scopes in [nil, []],
|
||||
do: {:error, :missing_scopes}
|
||||
|
||||
def validate(scopes, app_scopes) do
|
||||
case Pleroma.Plugs.OAuthScopesPlug.filter_descendants(scopes, app_scopes) do
|
||||
def validate(scopes, app_scopes, %User{} = user) do
|
||||
with {:ok, _} <- ensure_scopes_support(scopes, app_scopes),
|
||||
{:ok, scopes} <- authorize_admin_scopes(scopes, app_scopes, user) do
|
||||
{:ok, scopes}
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_scopes_support(scopes, app_scopes) do
|
||||
case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do
|
||||
^scopes -> {:ok, scopes}
|
||||
_ -> {:error, :unsupported_scopes}
|
||||
end
|
||||
end
|
||||
|
||||
defp authorize_admin_scopes(scopes, app_scopes, %User{} = user) do
|
||||
if user.is_admin || !contains_admin_scopes?(scopes) || !contains_admin_scopes?(app_scopes) do
|
||||
{:ok, scopes}
|
||||
else
|
||||
{:error, :unsupported_scopes}
|
||||
end
|
||||
end
|
||||
|
||||
def contains_admin_scopes?(scopes) do
|
||||
scopes
|
||||
|> OAuthScopesPlug.filter_descendants(["admin"])
|
||||
|> Enum.any?()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,11 +11,6 @@ defmodule Pleroma.Web.OAuth.Token.CleanWorker do
|
|||
@ten_seconds 10_000
|
||||
@one_day 86_400_000
|
||||
|
||||
@interval Pleroma.Config.get(
|
||||
[:oauth2, :clean_expired_tokens_interval],
|
||||
@one_day
|
||||
)
|
||||
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.Workers.BackgroundWorker
|
||||
|
||||
|
@ -29,8 +24,9 @@ def init(_) do
|
|||
@doc false
|
||||
def handle_info(:perform, state) do
|
||||
BackgroundWorker.enqueue("clean_expired_tokens", %{})
|
||||
interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day)
|
||||
|
||||
Process.send_after(self(), :perform, @interval)
|
||||
Process.send_after(self(), :perform, interval)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
|
|||
|
||||
@doc "POST /api/v1/pleroma/accounts/:id/subscribe"
|
||||
def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
|
||||
with {:ok, subscription_target} <- User.subscribe(user, subscription_target) do
|
||||
with {:ok, _subscription} <- User.subscribe(user, subscription_target) do
|
||||
render(conn, "relationship.json", user: user, target: subscription_target)
|
||||
else
|
||||
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||
|
@ -153,7 +153,7 @@ def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _p
|
|||
|
||||
@doc "POST /api/v1/pleroma/accounts/:id/unsubscribe"
|
||||
def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
|
||||
with {:ok, subscription_target} <- User.unsubscribe(user, subscription_target) do
|
||||
with {:ok, _subscription} <- User.unsubscribe(user, subscription_target) do
|
||||
render(conn, "relationship.json", user: user, target: subscription_target)
|
||||
else
|
||||
{:error, message} -> json_response(conn, :forbidden, %{error: message})
|
||||
|
|
|
@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
|
|||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write"]}
|
||||
%{scopes: ["write"], admin: true}
|
||||
when action in [
|
||||
:create,
|
||||
:delete,
|
||||
|
|
|
@ -16,14 +16,14 @@ defmodule Pleroma.Web.Push.Impl do
|
|||
require Logger
|
||||
import Ecto.Query
|
||||
|
||||
@types ["Create", "Follow", "Announce", "Like"]
|
||||
@types ["Create", "Follow", "Announce", "Like", "Move"]
|
||||
|
||||
@doc "Performs sending notifications for user subscriptions"
|
||||
@spec perform(Notification.t()) :: list(any) | :error
|
||||
def perform(
|
||||
%{
|
||||
activity: %{data: %{"type" => activity_type}, id: activity_id} = activity,
|
||||
user_id: user_id
|
||||
activity: %{data: %{"type" => activity_type}} = activity,
|
||||
user: %User{id: user_id}
|
||||
} = notif
|
||||
)
|
||||
when activity_type in @types do
|
||||
|
@ -39,18 +39,17 @@ def perform(
|
|||
for subscription <- fetch_subsriptions(user_id),
|
||||
get_in(subscription.data, ["alerts", type]) do
|
||||
%{
|
||||
title: format_title(notif),
|
||||
access_token: subscription.token.token,
|
||||
body: format_body(notif, actor, object),
|
||||
notification_id: notif.id,
|
||||
notification_type: type,
|
||||
icon: avatar_url,
|
||||
preferred_locale: "en",
|
||||
pleroma: %{
|
||||
activity_id: activity_id,
|
||||
activity_id: notif.activity.id,
|
||||
direct_conversation_id: direct_conversation_id
|
||||
}
|
||||
}
|
||||
|> Map.merge(build_content(notif, actor, object))
|
||||
|> Jason.encode!()
|
||||
|> push_message(build_sub(subscription), gcm_api_key, subscription)
|
||||
end
|
||||
|
@ -100,6 +99,24 @@ def build_sub(subscription) do
|
|||
}
|
||||
end
|
||||
|
||||
def build_content(
|
||||
%{
|
||||
activity: %{data: %{"directMessage" => true}},
|
||||
user: %{notification_settings: %{privacy_option: true}}
|
||||
},
|
||||
actor,
|
||||
_
|
||||
) do
|
||||
%{title: "New Direct Message", body: "@#{actor.nickname}"}
|
||||
end
|
||||
|
||||
def build_content(notif, actor, object) do
|
||||
%{
|
||||
title: format_title(notif),
|
||||
body: format_body(notif, actor, object)
|
||||
}
|
||||
end
|
||||
|
||||
def format_body(
|
||||
%{activity: %{data: %{"type" => "Create"}}},
|
||||
actor,
|
||||
|
|
|
@ -529,7 +529,10 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
get("/users/:nickname/feed", Feed.FeedController, :feed)
|
||||
get("/users/:nickname", Feed.FeedController, :feed_redirect)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web do
|
||||
pipe_through(:browser)
|
||||
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
|
||||
end
|
||||
|
||||
|
|
|
@ -129,16 +129,17 @@ defp do_stream(%{topic: topic, item: item}) do
|
|||
end
|
||||
|
||||
defp should_send?(%User{} = user, %Activity{} = item) do
|
||||
blocks = user.blocks || []
|
||||
mutes = user.mutes || []
|
||||
reblog_mutes = user.muted_reblogs || []
|
||||
recipient_blocks = MapSet.new(blocks ++ mutes)
|
||||
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
|
||||
User.outgoing_relations_ap_ids(user, [:block, :mute, :reblog_mute])
|
||||
|
||||
recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids)
|
||||
recipients = MapSet.new(item.recipients)
|
||||
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
|
||||
|
||||
with parent <- Object.normalize(item) || item,
|
||||
true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)),
|
||||
true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)),
|
||||
true <-
|
||||
Enum.all?([blocked_ap_ids, muted_ap_ids, reblog_muted_ap_ids], &(item.actor not in &1)),
|
||||
true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),
|
||||
true <- MapSet.disjoint?(recipients, recipient_blocks),
|
||||
%{host: item_host} <- URI.parse(item.actor),
|
||||
%{host: parent_host} <- URI.parse(parent.data["actor"]),
|
||||
|
|
|
@ -71,4 +71,11 @@ def perform(%{"op" => "fetch_data_for_activity", "activity_id" => activity_id},
|
|||
activity = Activity.get_by_id(activity_id)
|
||||
Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity)
|
||||
end
|
||||
|
||||
def perform(%{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}, _) do
|
||||
origin = User.get_cached_by_id(origin_id)
|
||||
target = User.get_cached_by_id(target_id)
|
||||
|
||||
Pleroma.FollowingRelationship.move_following(origin, target)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ def perform(%{"op" => "web_push", "notification_id" => notification_id}, _job) d
|
|||
notification =
|
||||
Notification
|
||||
|> Repo.get(notification_id)
|
||||
|> Repo.preload([:activity])
|
||||
|> Repo.preload([:activity, :user])
|
||||
|
||||
Pleroma.Web.Push.Impl.perform(notification)
|
||||
end
|
||||
|
|
1
mix.exs
1
mix.exs
|
@ -100,6 +100,7 @@ defp deps do
|
|||
{:plug_cowboy, "~> 2.0"},
|
||||
{:phoenix_pubsub, "~> 1.1"},
|
||||
{:phoenix_ecto, "~> 4.0"},
|
||||
{:ecto_enum, "~> 1.4"},
|
||||
{:ecto_sql, "~> 3.2"},
|
||||
{:postgrex, ">= 0.13.5"},
|
||||
{:oban, "~> 0.12.0"},
|
||||
|
|
5
mix.lock
5
mix.lock
|
@ -24,6 +24,7 @@
|
|||
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"},
|
||||
"earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"},
|
||||
"ecto": {:hex, :ecto, "3.2.5", "76c864b77948a479e18e69cc1d0f0f4ee7cced1148ffe6a093ff91eba644f0b5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.2.2", "d10845bc147b9f61ef485cbf0973c0a337237199bd9bd30dd9542db00aadc26b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0 or ~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"},
|
||||
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"},
|
||||
|
@ -35,8 +36,8 @@
|
|||
"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_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]},
|
||||
"excoveralls": {:hex, :excoveralls, "0.11.2", "0c6f2c8db7683b0caa9d490fb8125709c54580b4255ffa7ad35f3264b075a643", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"fast_html": {:hex, :fast_html, "0.99.4", "d80812664f0429607e1d880fba0ef04da87a2e4fa596701bcaae17953535695c", [:make, :mix], [], "hexpm"},
|
||||
"fast_sanitize": {:hex, :fast_sanitize, "0.1.4", "6c2e7203ca2f8275527a3021ba6e9d5d4ee213a47dc214a97c128737c9e56df1", [:mix], [{:fast_html, "~> 0.99", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"fast_html": {:hex, :fast_html, "1.0.1", "5bc7df4dc4607ec2c314c16414e4111d79a209956c4f5df96602d194c61197f9", [:make, :mix], [], "hexpm"},
|
||||
"fast_sanitize": {:hex, :fast_sanitize, "0.1.6", "60a5ae96879956dea409a91a77f5dd2994c24cc10f80eefd8f9892ee4c0c7b25", [:mix], [{:fast_html, "~> 1.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"floki": {:hex, :floki, "0.23.1", "e100306ce7d8841d70a559748e5091542e2cfc67ffb3ade92b89a8435034dab1", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm"},
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddMoveSupportToUsers do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
add(:also_known_as, {:array, :string}, default: [], null: false)
|
||||
add(:allow_following_move, :boolean, default: true, null: false)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
defmodule Pleroma.Repo.Migrations.CreateUserRelationships do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create_if_not_exists table(:user_relationships) do
|
||||
add(:source_id, references(:users, type: :uuid, on_delete: :delete_all))
|
||||
add(:target_id, references(:users, type: :uuid, on_delete: :delete_all))
|
||||
add(:relationship_type, :integer, null: false)
|
||||
|
||||
timestamps(updated_at: false)
|
||||
end
|
||||
|
||||
create_if_not_exists(
|
||||
unique_index(:user_relationships, [:source_id, :relationship_type, :target_id])
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,68 @@
|
|||
defmodule Pleroma.Repo.Migrations.DataMigrationPopulateUserRelationships do
|
||||
use Ecto.Migration
|
||||
|
||||
alias Ecto.Adapters.SQL
|
||||
alias Pleroma.Repo
|
||||
|
||||
require Logger
|
||||
|
||||
def up do
|
||||
Enum.each(
|
||||
[blocks: 1, mutes: 2, muted_reblogs: 3, muted_notifications: 4, subscribers: 5],
|
||||
fn {field, relationship_type_code} ->
|
||||
migrate(field, relationship_type_code)
|
||||
|
||||
if field == :subscribers do
|
||||
drop_if_exists(index(:users, [:subscribers]))
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def down, do: :noop
|
||||
|
||||
defp migrate(field, relationship_type_code) do
|
||||
Logger.info("Processing users.#{field}...")
|
||||
|
||||
{:ok, %{rows: field_rows}} =
|
||||
SQL.query(Repo, "SELECT id, #{field} FROM users WHERE #{field} != '{}'")
|
||||
|
||||
target_ap_ids =
|
||||
Enum.flat_map(
|
||||
field_rows,
|
||||
fn [_, ap_ids] -> ap_ids end
|
||||
)
|
||||
|> Enum.uniq()
|
||||
|
||||
# Selecting ids of all targets at once in order to reduce the number of SELECT queries
|
||||
{:ok, %{rows: target_ap_id_id}} =
|
||||
SQL.query(Repo, "SELECT ap_id, id FROM users WHERE ap_id = ANY($1)", [target_ap_ids])
|
||||
|
||||
target_id_by_ap_id = Enum.into(target_ap_id_id, %{}, fn [k, v] -> {k, v} end)
|
||||
|
||||
Enum.each(
|
||||
field_rows,
|
||||
fn [source_id, target_ap_ids] ->
|
||||
source_uuid = Ecto.UUID.cast!(source_id)
|
||||
|
||||
for target_ap_id <- target_ap_ids do
|
||||
target_id = target_id_by_ap_id[target_ap_id]
|
||||
|
||||
with {:ok, target_uuid} <- target_id && Ecto.UUID.cast(target_id) do
|
||||
execute("""
|
||||
INSERT INTO user_relationships(
|
||||
source_id, target_id, relationship_type, inserted_at
|
||||
)
|
||||
VALUES(
|
||||
'#{source_uuid}'::uuid, '#{target_uuid}'::uuid, #{relationship_type_code}, now()
|
||||
)
|
||||
ON CONFLICT (source_id, relationship_type, target_id) DO NOTHING
|
||||
""")
|
||||
else
|
||||
_ -> Logger.warn("Unresolved #{field} reference: (#{source_uuid}, #{target_id})")
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddActivitypubActorType do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table("users") do
|
||||
add(:actor_type, :string, null: false, default: "Person")
|
||||
end
|
||||
end
|
||||
end
|
93
priv/scrubbers/default.ex
Normal file
93
priv/scrubbers/default.ex
Normal file
|
@ -0,0 +1,93 @@
|
|||
defmodule Pleroma.HTML.Scrubber.Default do
|
||||
@doc "The default HTML scrubbing policy: no "
|
||||
|
||||
require FastSanitize.Sanitizer.Meta
|
||||
alias FastSanitize.Sanitizer.Meta
|
||||
|
||||
# credo:disable-for-previous-line
|
||||
# No idea how to fix this one…
|
||||
|
||||
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
|
||||
|
||||
Meta.strip_comments()
|
||||
|
||||
Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes)
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:a, "class", [
|
||||
"hashtag",
|
||||
"u-url",
|
||||
"mention",
|
||||
"u-url mention",
|
||||
"mention u-url"
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:a, "rel", [
|
||||
"tag",
|
||||
"nofollow",
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"ugc"
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:a, ["name", "title"])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:abbr, ["title"])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:b, [])
|
||||
Meta.allow_tag_with_these_attributes(:blockquote, [])
|
||||
Meta.allow_tag_with_these_attributes(:br, [])
|
||||
Meta.allow_tag_with_these_attributes(:code, [])
|
||||
Meta.allow_tag_with_these_attributes(:del, [])
|
||||
Meta.allow_tag_with_these_attributes(:em, [])
|
||||
Meta.allow_tag_with_these_attributes(:i, [])
|
||||
Meta.allow_tag_with_these_attributes(:li, [])
|
||||
Meta.allow_tag_with_these_attributes(:ol, [])
|
||||
Meta.allow_tag_with_these_attributes(:p, [])
|
||||
Meta.allow_tag_with_these_attributes(:pre, [])
|
||||
Meta.allow_tag_with_these_attributes(:strong, [])
|
||||
Meta.allow_tag_with_these_attributes(:sub, [])
|
||||
Meta.allow_tag_with_these_attributes(:sup, [])
|
||||
Meta.allow_tag_with_these_attributes(:u, [])
|
||||
Meta.allow_tag_with_these_attributes(:ul, [])
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"])
|
||||
Meta.allow_tag_with_these_attributes(:span, [])
|
||||
|
||||
@allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images])
|
||||
|
||||
if @allow_inline_images do
|
||||
# restrict img tags to http/https only, because of MediaProxy.
|
||||
Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:img, [
|
||||
"width",
|
||||
"height",
|
||||
"class",
|
||||
"title",
|
||||
"alt"
|
||||
])
|
||||
end
|
||||
|
||||
if Pleroma.Config.get([:markup, :allow_tables]) do
|
||||
Meta.allow_tag_with_these_attributes(:table, [])
|
||||
Meta.allow_tag_with_these_attributes(:tbody, [])
|
||||
Meta.allow_tag_with_these_attributes(:td, [])
|
||||
Meta.allow_tag_with_these_attributes(:th, [])
|
||||
Meta.allow_tag_with_these_attributes(:thead, [])
|
||||
Meta.allow_tag_with_these_attributes(:tr, [])
|
||||
end
|
||||
|
||||
if Pleroma.Config.get([:markup, :allow_headings]) do
|
||||
Meta.allow_tag_with_these_attributes(:h1, [])
|
||||
Meta.allow_tag_with_these_attributes(:h2, [])
|
||||
Meta.allow_tag_with_these_attributes(:h3, [])
|
||||
Meta.allow_tag_with_these_attributes(:h4, [])
|
||||
Meta.allow_tag_with_these_attributes(:h5, [])
|
||||
end
|
||||
|
||||
if Pleroma.Config.get([:markup, :allow_fonts]) do
|
||||
Meta.allow_tag_with_these_attributes(:font, ["face"])
|
||||
end
|
||||
|
||||
Meta.strip_everything_not_covered()
|
||||
end
|
27
priv/scrubbers/links_only.ex
Normal file
27
priv/scrubbers/links_only.ex
Normal file
|
@ -0,0 +1,27 @@
|
|||
defmodule Pleroma.HTML.Scrubber.LinksOnly do
|
||||
@moduledoc """
|
||||
An HTML scrubbing policy which limits to links only.
|
||||
"""
|
||||
|
||||
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
|
||||
|
||||
require FastSanitize.Sanitizer.Meta
|
||||
alias FastSanitize.Sanitizer.Meta
|
||||
|
||||
Meta.strip_comments()
|
||||
|
||||
# links
|
||||
Meta.allow_tag_with_uri_attributes(:a, ["href"], @valid_schemes)
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:a, "rel", [
|
||||
"tag",
|
||||
"nofollow",
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"me",
|
||||
"ugc"
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:a, ["name", "title"])
|
||||
Meta.strip_everything_not_covered()
|
||||
end
|
32
priv/scrubbers/media_proxy.ex
Normal file
32
priv/scrubbers/media_proxy.ex
Normal file
|
@ -0,0 +1,32 @@
|
|||
defmodule Pleroma.HTML.Transform.MediaProxy do
|
||||
@moduledoc "Transforms inline image URIs to use MediaProxy."
|
||||
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
def before_scrub(html), do: html
|
||||
|
||||
def scrub_attribute(:img, {"src", "http" <> target}) do
|
||||
media_url =
|
||||
("http" <> target)
|
||||
|> MediaProxy.url()
|
||||
|
||||
{"src", media_url}
|
||||
end
|
||||
|
||||
def scrub_attribute(_tag, attribute), do: attribute
|
||||
|
||||
def scrub({:img, attributes, children}) do
|
||||
attributes =
|
||||
attributes
|
||||
|> Enum.map(fn attr -> scrub_attribute(:img, attr) end)
|
||||
|> Enum.reject(&is_nil(&1))
|
||||
|
||||
{:img, attributes, children}
|
||||
end
|
||||
|
||||
def scrub({:comment, _text, _children}), do: ""
|
||||
|
||||
def scrub({tag, attributes, children}), do: {tag, attributes, children}
|
||||
def scrub({_tag, children}), do: children
|
||||
def scrub(text), do: text
|
||||
end
|
57
priv/scrubbers/twitter_text.ex
Normal file
57
priv/scrubbers/twitter_text.ex
Normal file
|
@ -0,0 +1,57 @@
|
|||
defmodule Pleroma.HTML.Scrubber.TwitterText do
|
||||
@moduledoc """
|
||||
An HTML scrubbing policy which limits to twitter-style text. Only
|
||||
paragraphs, breaks and links are allowed through the filter.
|
||||
"""
|
||||
|
||||
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
|
||||
|
||||
require FastSanitize.Sanitizer.Meta
|
||||
alias FastSanitize.Sanitizer.Meta
|
||||
|
||||
Meta.strip_comments()
|
||||
|
||||
# links
|
||||
Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes)
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:a, "class", [
|
||||
"hashtag",
|
||||
"u-url",
|
||||
"mention",
|
||||
"u-url mention",
|
||||
"mention u-url"
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:a, "rel", [
|
||||
"tag",
|
||||
"nofollow",
|
||||
"noopener",
|
||||
"noreferrer"
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:a, ["name", "title"])
|
||||
|
||||
# paragraphs and linebreaks
|
||||
Meta.allow_tag_with_these_attributes(:br, [])
|
||||
Meta.allow_tag_with_these_attributes(:p, [])
|
||||
|
||||
# microformats
|
||||
Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"])
|
||||
Meta.allow_tag_with_these_attributes(:span, [])
|
||||
|
||||
# allow inline images for custom emoji
|
||||
if Pleroma.Config.get([:markup, :allow_inline_images]) do
|
||||
# restrict img tags to http/https only, because of MediaProxy.
|
||||
Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:img, [
|
||||
"width",
|
||||
"height",
|
||||
"class",
|
||||
"title",
|
||||
"alt"
|
||||
])
|
||||
end
|
||||
|
||||
Meta.strip_everything_not_covered()
|
||||
end
|
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
.prop-row{margin-bottom:1em}.emoji-preview-img{max-width:5em}.copy-to-local-button{margin-top:2em;float:right}.new-emoji-col{margin-top:8em}.or,.shared-pack-dl-box{margin:1em}.dl-as-input{margin:1em;max-width:30%}.contents-collapse{margin:1em}.pack-actions{margin-top:1em}.new-emoji-uploader{margin-bottom:3em}.emoji-packs-container{margin:22px 0 0 15px}.local-packs-actions{margin-top:1em;margin-bottom:1em}.remote-instance-input{max-width:10%}.create-pack-button{margin-top:1em}
|
|
@ -1 +0,0 @@
|
|||
a{text-decoration:underline}.code{background-color:rgba(173,190,214,.48);border-radius:3px;font-family:monospace;padding:0 3px}.el-form-item{margin-right:30px}.el-select{width:100%}.esshd-list{margin:0}.expl{color:#666;font-size:13px;line-height:22px;margin:5px 0 0;overflow-wrap:break-word}.highlight{background-color:#e6e6e6}.limit-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.limit-expl{margin-left:10px}.limit-input{width:48%;margin:0 0 5px 8px}.line{width:100%;height:0;border:1px solid #eee;margin-bottom:22px}.mascot-container{margin-bottom:15px}.mascot-input{margin-bottom:7px}.mascot-name-container{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:7px}.mascot-name-input{margin-right:10px}.name-input{width:30%;margin-right:8px}.options-paragraph{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;line-height:20px;margin:0 0 14px}.options-paragraph-container{overflow-wrap:break-word;margin-bottom:0}.pattern-input{width:20%;margin-right:8px}.setting-input{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.single-input{margin-right:10px}.scale-input{width:48%;margin:0 8px 5px 0}.replacement-input{width:80%;margin-left:8px;margin-right:10px}.text{line-height:20px;margin-right:15px}.upload-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.value-input{width:70%;margin-left:8px;margin-right:10px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.el-form-item{margin-right:15px}.el-input__inner{padding:0 5px}.el-form-item__label:not(.no-top-margin){padding-left:3px;padding-right:10px;line-height:22px;margin-top:7px}.el-message{min-width:80%}.el-select__tags{overflow:hidden}.name-input{width:40%;margin-right:5px}p.expl{line-height:20px}.pattern-input{width:40%;margin-right:4px}.replacement-input{width:60%;margin-left:4px;margin-right:5px}.top-margin{position:absolute;top:25%}.value-input{width:60%;margin-left:5px;margin-right:8px}}.settings-container .el-tabs[data-v-729534ce]{margin-top:20px}.settings-container h1[data-v-729534ce]{margin:22px 0 0 15px}
|
|
@ -1 +0,0 @@
|
|||
header[data-v-71c7ded0]{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;margin:22px 0;padding-left:15px}header h1[data-v-71c7ded0]{margin:0 0 0 10px}table[data-v-71c7ded0]{margin:10px 0 0 15px}table .name-col[data-v-71c7ded0]{width:150px}.el-table--border[data-v-71c7ded0]:after,.el-table--group[data-v-71c7ded0]:after,.el-table[data-v-71c7ded0]:before{background-color:transparent}.poll ul[data-v-71c7ded0]{list-style-type:none;padding:0;width:30%}.image[data-v-71c7ded0]{width:20%}.image img[data-v-71c7ded0]{width:100%}.statuses[data-v-71c7ded0]{padding-right:20px}.show-private[data-v-71c7ded0]{text-align:right;line-height:67px;padding-right:20px}
|
|
@ -1 +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}}.actions-button[data-v-a2aed82c]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-a2aed82c]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-a2aed82c]{float:right}.el-icon-edit[data-v-a2aed82c]{margin-right:5px}.tag-container[data-v-a2aed82c]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-a2aed82c]{padding-right:20px}.no-hover[data-v-a2aed82c]:hover{color:#606266;background-color:#fff;cursor:auto}.el-dialog__body{padding:20px}.create-account-form-item{margin-bottom:20px}.create-account-form-item-without-margin{margin-bottom:0}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.create-user-dialog{width:85%}.create-account-form-item{margin-bottom:20px}.el-dialog__body{padding:20px}}.actions-button{text-align:left;width:350px;padding:10px}.actions-container{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:0 15px 10px}.active-tag{color:#409eff;font-weight:700}.active-tag .el-icon-check{color:#409eff;float:right;margin:7px 0 0 15px}.el-dropdown-link:hover{cursor:pointer;color:#409eff}.el-icon-plus{margin-right:5px}.password-reset-token{margin:0 0 14px}.password-reset-token-dialog{width:50%}.reset-password-link{text-decoration:underline}.users-container h1{margin:22px 0 0 15px}.users-container .pagination{margin:25px 0;text-align:center}.users-container .search{width:350px;float:right}.users-container .filter-container{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 15px}.users-container .user-count{color:grey;font-size:28px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.password-reset-token-dialog{width:85%}.users-container h1{margin:7px 10px 15px}.users-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px 7px}.users-container .create-account{width:100%}.users-container .el-icon-arrow-down{font-size:12px}.users-container .search{width:100%}.users-container .filter-container{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}.users-container .el-tag{width:30px;display:inline-block;margin-bottom:4px;font-weight:700}.users-container .el-tag.el-tag--danger,.users-container .el-tag.el-tag--success{padding-left:8px}}
|
||||
.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}}.actions-button[data-v-56aa3725]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-56aa3725]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-56aa3725]{float:right}.el-icon-edit[data-v-56aa3725]{margin-right:5px}.tag-container[data-v-56aa3725]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-56aa3725]{padding-right:20px}.no-hover[data-v-56aa3725]:hover{color:#606266;background-color:#fff;cursor:auto}.el-dialog__body{padding:20px}.create-account-form-item{margin-bottom:20px}.create-account-form-item-without-margin{margin-bottom:0}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.create-user-dialog{width:85%}.create-account-form-item{margin-bottom:20px}.el-dialog__body{padding:20px}}.actions-button{text-align:left;width:350px;padding:10px}.actions-container{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:0 15px 10px}.active-tag{color:#409eff;font-weight:700}.active-tag .el-icon-check{color:#409eff;float:right;margin:7px 0 0 15px}.el-dropdown-link:hover{cursor:pointer;color:#409eff}.el-icon-plus{margin-right:5px}.password-reset-token{margin:0 0 14px}.password-reset-token-dialog{width:50%}.reset-password-link{text-decoration:underline}.users-container h1{margin:22px 0 0 15px}.users-container .pagination{margin:25px 0;text-align:center}.users-container .search{width:350px;float:right}.users-container .filter-container{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 15px}.users-container .user-count{color:grey;font-size:28px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.password-reset-token-dialog{width:85%}.users-container h1{margin:7px 10px 15px}.users-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px 7px}.users-container .create-account{width:100%}.users-container .el-icon-arrow-down{font-size:12px}.users-container .search{width:100%}.users-container .filter-container{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}.users-container .el-tag{width:30px;display:inline-block;margin-bottom:4px;font-weight:700}.users-container .el-tag.el-tag--danger,.users-container .el-tag.el-tag--success{padding-left:8px}}
|
1
priv/static/adminfe/chunk-4bb1.1fa93c2f.css
Normal file
1
priv/static/adminfe/chunk-4bb1.1fa93c2f.css
Normal file
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
.status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-avatar-img{width:15px;height:15px;margin-right:5px}.status-account-name{margin:0;height:22px}.status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-content{font-size:15px}.status-card{margin-bottom:15px}.status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.account{text-decoration:underline}.avatar-img{vertical-align:bottom;width:15px;height:15px;margin-left:5px}.el-card__body{padding:17px}.el-card__header{background-color:#fafafa;padding:10px 20px}.el-collapse{border-bottom:none}.el-collapse-item__header{height:46px;font-size:14px}.el-collapse-item__content{padding-bottom:7px}.el-icon-arrow-right{margin-right:6px}.el-icon-close{padding:10px 5px 10px 10px;cursor:pointer}h4{margin:0;height:17px}.header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;height:40px}.id{color:grey;margin-top:6px}.line{width:100%;height:0;border:.5px solid #ebeef5;margin:15px 0}.new-note p{font-size:14px;font-weight:500;height:17px;margin:13px 0 7px}.note{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,.1);box-shadow:0 2px 5px 0 rgba(0,0,0,.1);margin-bottom:10px}.no-notes{font-style:italic;color:grey}.report-row-key{font-weight:500;font-size:14px}.report-title{margin:0}.statuses{margin-top:15px}.submit-button{display:block;margin:7px 0 17px auto}.timestamp{margin:0;font-style:italic;color:grey}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.timeline-item-container .header-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;height:80px}.timeline-item-container .id{margin:6px 0 0}}.select-field[data-v-07695bc4]{width:350px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.select-field[data-v-07695bc4]{width:100%;margin-bottom:5px}}.reports-container .el-timeline[data-v-e32c7dc6]{margin:45px 45px 45px 19px;padding:0}.reports-container .filter-container[data-v-e32c7dc6]{margin:22px 15px;padding-bottom:0}.reports-container h1[data-v-e32c7dc6]{margin:22px 0 0 15px}.reports-container .no-reports-message[data-v-e32c7dc6]{color:grey;margin-left:19px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.reports-container h1[data-v-e32c7dc6]{margin:7px 10px 15px}.reports-container .filter-container[data-v-e32c7dc6]{margin:0 10px}.reports-container .timeline[data-v-e32c7dc6]{margin:20px 20px 20px 18px}}
|
1
priv/static/adminfe/chunk-5668.fef62a66.css
Normal file
1
priv/static/adminfe/chunk-5668.fef62a66.css
Normal file
|
@ -0,0 +1 @@
|
|||
.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{width:15px;height:15px;margin-right:5px}.status-card .status-account-name{margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-card{margin-bottom:15px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}.statuses-container{padding:0 15px}.filter-container{margin:22px 15px 15px 0}.statuses-pagination{padding:15px 0;text-align:center}h1{margin:22px 0 0}
|
|
@ -1 +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 .login-button{width:100%;margin:0 0 10px}.login-container .omit-host-note{color:#596f8c;font-size:.8em;font-style:italic;margin:-20px 0 15px;padding:3px 0 0 15px}.login-container[data-v-d027d802]{min-height:100%;width:100%;background-color:#2d3a4b;overflow:hidden}.login-container .login-form[data-v-d027d802]{position:relative;width:520px;max-width:100%;padding:160px 35px 0;margin:0 auto;overflow:hidden}.login-container .tips[data-v-d027d802]{font-size:14px;color:#fff;margin-bottom:10px}.login-container .tips span[data-v-d027d802]:first-of-type{margin-right:16px}.login-container .svg-container[data-v-d027d802]{padding:6px 5px 6px 15px;color:#889aa4;vertical-align:middle;width:30px;display:inline-block}.login-container .title-container[data-v-d027d802]{position:relative}.login-container .title-container .title[data-v-d027d802]{font-size:26px;color:#eee;margin:0 auto 40px;text-align:center;font-weight:700}.login-container .title-container .set-language[data-v-d027d802]{color:#fff;position:absolute;top:3px;font-size:18px;right:0;cursor:pointer}.login-container .show-pwd[data-v-d027d802]{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-d027d802]{position:absolute;right:0;bottom:6px}
|
||||
@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 .login-button{width:100%;margin:0 0 10px}.login-container .omit-host-note{color:#596f8c;font-size:.8em;font-style:italic;margin:-20px 0 15px;padding:3px 0 0 15px}.login-container[data-v-5bb13616]{min-height:100%;width:100%;background-color:#2d3a4b;overflow:hidden}.login-container .login-form[data-v-5bb13616]{position:relative;width:520px;max-width:100%;padding:160px 35px 0;margin:0 auto;overflow:hidden}.login-container .tips[data-v-5bb13616]{font-size:14px;color:#fff;margin-bottom:10px}.login-container .tips span[data-v-5bb13616]:first-of-type{margin-right:16px}.login-container .svg-container[data-v-5bb13616]{padding:6px 5px 6px 15px;color:#889aa4;vertical-align:middle;width:30px;display:inline-block}.login-container .title-container[data-v-5bb13616]{position:relative}.login-container .title-container .title[data-v-5bb13616]{font-size:26px;color:#eee;margin:0 auto 40px;text-align:center;font-weight:700}.login-container .title-container .set-language[data-v-5bb13616]{color:#fff;position:absolute;top:3px;font-size:18px;right:0;cursor:pointer}.login-container .show-pwd[data-v-5bb13616]{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-5bb13616]{position:absolute;right:0;bottom:6px}
|
1
priv/static/adminfe/chunk-73af.cc4041dc.css
Normal file
1
priv/static/adminfe/chunk-73af.cc4041dc.css
Normal file
|
@ -0,0 +1 @@
|
|||
a{text-decoration:underline}.code{background-color:rgba(173,190,214,.48);border-radius:3px;font-family:monospace;padding:0 3px}.el-form-item{margin-right:30px}.el-select{width:100%}.esshd-list{margin:0}.expl{color:#666;font-size:13px;line-height:22px;margin:5px 0 0;overflow-wrap:break-word}.highlight{background-color:#e6e6e6}.limit-button-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.limit-expl{margin-left:10px}.limit-input{width:48%;margin:0 0 5px 8px}.mascot-container{margin-bottom:15px}.mascot-input{margin-bottom:7px}.mascot-name-container{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:7px}.mascot-name-input{margin-right:10px}.name-input{width:30%;margin-right:8px}.options-paragraph{font-size:14px;color:#606266;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei;font-weight:700;line-height:20px;margin:0 0 14px}.options-paragraph-container{overflow-wrap:break-word;margin-bottom:0}.pattern-input{width:20%;margin-right:8px}.setting-input{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.single-input{margin-right:10px}.scale-input{width:48%;margin:0 8px 5px 0}.replacement-input{width:80%}.replacement-input,.value-input{margin-left:8px;margin-right:10px}.value-input{width:70%}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.el-form-item{margin-right:15px}.el-input__inner{padding:0 5px}.el-form-item__label:not(.no-top-margin){padding-left:3px;padding-right:10px;line-height:22px;margin-top:7px}.el-message{min-width:80%}.el-select__tags{overflow:hidden}.name-input{width:40%;margin-right:5px}p.expl{line-height:20px}.pattern-input{width:40%;margin-right:4px}.replacement-input{width:60%;margin-left:4px;margin-right:5px}.top-margin{position:absolute;top:25%}.value-input{width:60%;margin-left:5px;margin-right:8px}}.copy-popover{width:330px}.emoji-button{margin-left:10px}.emoji-buttons{min-width:210px}.emoji-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:10px}.emoji-preview-img{max-width:5em}.emoji-info{margin-left:10px}.copy-to-local-button{margin-top:12px;float:right}.add-new-emoji{height:36px;font-size:14px;font-weight:700;color:#606266}.text{line-height:20px;margin-right:15px}.upload-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.upload-button{margin-left:10px}.upload-file-url{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.download-archive{width:250px}.download-shared-pack{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px}.download-shared-pack-button{margin-left:10px}.el-collapse-item__content{padding-bottom:0}.el-collapse-item__header{height:36px;font-size:14px;font-weight:700;color:#606266}.emoji-pack-card{margin-top:5px}.emoji-pack-metadata .el-form-item{margin-bottom:10px}.has-background .el-collapse-item__header{background:#f6f6f6}.no-background .el-collapse-item__header{background:#fff}.save-pack-button{margin-bottom:5px}.button-container{margin:0 0 22px 20px}.create-pack{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.create-pack-button{margin-left:10px}.line{width:100%;height:0;border:1px solid #eee;margin-bottom:22px}.settings-container .el-tabs[data-v-c56a6d00]{margin-top:20px}.settings-container h1[data-v-c56a6d00]{margin:22px 0 0 15px}
|
1
priv/static/adminfe/chunk-ce58.e5b0a61d.css
Normal file
1
priv/static/adminfe/chunk-ce58.e5b0a61d.css
Normal file
|
@ -0,0 +1 @@
|
|||
.status-card .account{text-decoration:underline;line-height:26px;font-size:13px}.status-card .image{width:20%}.status-card .image img{width:100%}.status-card .show-more-button{margin-left:5px}.status-card .status-account{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.status-card .status-avatar-img{width:15px;height:15px;margin-right:5px}.status-card .status-account-name{margin:0;height:22px}.status-card .status-body{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.status-card .status-content{font-size:15px;line-height:26px}.status-card .status-card{margin-bottom:15px}.status-card .status-deleted{font-style:italic;margin-top:3px}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.status-card .status-without-content{font-style:italic}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.el-message{min-width:80%}.el-message-box{width:80%}.status-card .el-card__header{padding:10px 17px}.status-card .el-tag{margin:3px 4px 3px 0}.status-card .status-account-container{margin-bottom:5px}.status-card .status-actions-button{margin:3px 0}.status-card .status-actions{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.status-card .status-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}}header[data-v-1966c214]{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex;margin:22px 0;padding-left:15px}header h1[data-v-1966c214]{margin:0 0 0 10px}table[data-v-1966c214]{margin:10px 0 0 15px}table .name-col[data-v-1966c214]{width:150px}.el-table--border[data-v-1966c214]:after,.el-table--group[data-v-1966c214]:after,.el-table[data-v-1966c214]:before{background-color:transparent}.poll ul[data-v-1966c214]{list-style-type:none;padding:0;width:30%}.image[data-v-1966c214]{width:20%}.image img[data-v-1966c214]{width:100%}.no-statuses[data-v-1966c214]{margin-left:28px;color:#606266}.recent-statuses-header[data-v-1966c214]{margin-top:10px}.statuses[data-v-1966c214]{padding:0 20px 0 0}.show-private[data-v-1966c214]{text-align:right;line-height:67px;padding-right:20px}.recent-statuses[data-v-1966c214]{margin-left:28px}.user-profile-card[data-v-1966c214]{margin-left:15px;margin-right:20px}.user-profile-table[data-v-1966c214]{margin:0}.user-profile-tag[data-v-1966c214]{margin:0 4px 4px 0}
|
|
@ -1 +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=chunk-elementUI.a842fb0a.css rel=stylesheet><link href=chunk-libs.57fe98a3.css rel=stylesheet><link href=app.8589ec81.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.46db235c.js></script><script type=text/javascript src=static/js/chunk-elementUI.fa319e7b.js></script><script type=text/javascript src=static/js/chunk-libs.35c18287.js></script><script type=text/javascript src=static/js/app.9c4316f1.js></script></body></html>
|
||||
<!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=chunk-elementUI.fbcb853d.css rel=stylesheet><link href=chunk-libs.74279b9a.css rel=stylesheet><link href=app.2d7d6c47.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=static/js/runtime.5485cb1b.js></script><script type=text/javascript src=static/js/chunk-elementUI.adb56751.js></script><script type=text/javascript src=static/js/chunk-libs.16bdd178.js></script><script type=text/javascript src=static/js/app.9abe83aa.js></script></body></html>
|
9
priv/static/adminfe/static/js/ZhIB.861df339.js
Normal file
9
priv/static/adminfe/static/js/ZhIB.861df339.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/ZhIB.861df339.js.map
Normal file
1
priv/static/adminfe/static/js/ZhIB.861df339.js.map
Normal file
File diff suppressed because one or more lines are too long
2
priv/static/adminfe/static/js/app.9abe83aa.js
Normal file
2
priv/static/adminfe/static/js/app.9abe83aa.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/adminfe/static/js/app.9abe83aa.js.map
Normal file
1
priv/static/adminfe/static/js/app.9abe83aa.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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
Loading…
Reference in a new issue