Merge remote-tracking branch 'pleroma/develop' into features/poll-validation
This commit is contained in:
commit
ac2598307d
288 changed files with 3671 additions and 3020 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -27,6 +27,8 @@ erl_crash.dump
|
||||||
# variables.
|
# variables.
|
||||||
/config/*.secret.exs
|
/config/*.secret.exs
|
||||||
/config/generated_config.exs
|
/config/generated_config.exs
|
||||||
|
/config/*.env
|
||||||
|
|
||||||
|
|
||||||
# Database setup file, some may forget to delete it
|
# Database setup file, some may forget to delete it
|
||||||
/config/setup_db.psql
|
/config/setup_db.psql
|
||||||
|
|
|
@ -8,9 +8,7 @@
|
||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
* Installation type:
|
* Installation type (OTP or From Source):
|
||||||
- [ ] OTP
|
|
||||||
- [ ] From source
|
|
||||||
* Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE):
|
* Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE):
|
||||||
* Elixir version (`elixir -v` for from source installations, N/A for OTP):
|
* Elixir version (`elixir -v` for from source installations, N/A for OTP):
|
||||||
* Operating system:
|
* Operating system:
|
||||||
|
|
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -7,12 +7,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **Breaking:** Elixir >=1.9 is now required (was >= 1.8)
|
- **Breaking:** Elixir >=1.9 is now required (was >= 1.8)
|
||||||
|
- **Breaking:** Configuration: `:auto_linker, :opts` moved to `:pleroma, Pleroma.Formatter`. Old config namespace is deprecated.
|
||||||
- In Conversations, return only direct messages as `last_status`
|
- In Conversations, return only direct messages as `last_status`
|
||||||
- Using the `only_media` filter on timelines will now exclude reblog media
|
- Using the `only_media` filter on timelines will now exclude reblog media
|
||||||
- MFR policy to set global expiration for all local Create activities
|
- MFR policy to set global expiration for all local Create activities
|
||||||
- OGP rich media parser merged with TwitterCard
|
- OGP rich media parser merged with TwitterCard
|
||||||
- Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated.
|
- Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated.
|
||||||
- Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated.
|
- Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated.
|
||||||
|
- **Breaking:** Configuration: `:instance, welcome_user_nickname` moved to `:welcome, :direct_message, :sender_nickname`, `:instance, :welcome_message` moved to `:welcome, :direct_message, :message`. Old config namespace is deprecated.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>API Changes</summary>
|
<summary>API Changes</summary>
|
||||||
|
@ -25,6 +27,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance
|
- Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance
|
||||||
- Mastodon API: On deletion, returns the original post text.
|
- Mastodon API: On deletion, returns the original post text.
|
||||||
- Mastodon API: Add `pleroma.unread_count` to the Marker entity.
|
- Mastodon API: Add `pleroma.unread_count` to the Marker entity.
|
||||||
|
- **Breaking:** Notification Settings API for suppressing notifications
|
||||||
|
has been simplified down to `block_from_strangers`.
|
||||||
|
- **Breaking:** Notification Settings API option for hiding push notification
|
||||||
|
contents has been renamed to `hide_notification_contents`
|
||||||
|
- Mastodon API: Added `pleroma.metadata.post_formats` to /api/v1/instance
|
||||||
|
- Mastodon API (legacy): Allow query parameters for `/api/v1/domain_blocks`, e.g. `/api/v1/domain_blocks?domain=badposters.zone`
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
@ -59,10 +67,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Support pagination in emoji packs API (for packs and for files in pack)
|
- Support pagination in emoji packs API (for packs and for files in pack)
|
||||||
- Support for viewing instances favicons next to posts and accounts
|
- Support for viewing instances favicons next to posts and accounts
|
||||||
- Added Pleroma.Upload.Filter.Exiftool as an alternate EXIF stripping mechanism targeting GPS/location metadata.
|
- Added Pleroma.Upload.Filter.Exiftool as an alternate EXIF stripping mechanism targeting GPS/location metadata.
|
||||||
|
- "By approval" registrations mode.
|
||||||
|
- Configuration: Added `:welcome` settings for the welcome message to newly registered users.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>API Changes</summary>
|
<summary>API Changes</summary>
|
||||||
- Mastodon API: Add pleroma.parents_visible field to statuses.
|
|
||||||
|
- Mastodon API: Add pleroma.parent_visible field to statuses.
|
||||||
- Mastodon API: Extended `/api/v1/instance`.
|
- Mastodon API: Extended `/api/v1/instance`.
|
||||||
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
|
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
|
||||||
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
|
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
|
||||||
|
@ -87,6 +98,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Admin API: fix `GET /api/pleroma/admin/users/:nickname/credentials` returning 404 when getting the credentials of a remote user while `:instance, :limit_to_local_content` is set to `:unauthenticated`
|
- Admin API: fix `GET /api/pleroma/admin/users/:nickname/credentials` returning 404 when getting the credentials of a remote user while `:instance, :limit_to_local_content` is set to `:unauthenticated`
|
||||||
- Fix CSP policy generation to include remote Captcha services
|
- Fix CSP policy generation to include remote Captcha services
|
||||||
- Fix edge case where MediaProxy truncates media, usually caused when Caddy is serving content for the other Federated instance.
|
- Fix edge case where MediaProxy truncates media, usually caused when Caddy is serving content for the other Federated instance.
|
||||||
|
- Emoji Packs could not be listed when instance was set to `public: false`
|
||||||
|
|
||||||
## [Unreleased (patch)]
|
## [Unreleased (patch)]
|
||||||
|
|
||||||
|
@ -116,6 +128,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Follow request notifications
|
- Follow request notifications
|
||||||
<details>
|
<details>
|
||||||
<summary>API Changes</summary>
|
<summary>API Changes</summary>
|
||||||
|
|
||||||
- Admin API: `GET /api/pleroma/admin/need_reboot`.
|
- Admin API: `GET /api/pleroma/admin/need_reboot`.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
@ -183,6 +196,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- **Breaking**: Using third party engines for user recommendation
|
- **Breaking**: Using third party engines for user recommendation
|
||||||
<details>
|
<details>
|
||||||
<summary>API Changes</summary>
|
<summary>API Changes</summary>
|
||||||
|
|
||||||
- **Breaking**: AdminAPI: migrate_from_db endpoint
|
- **Breaking**: AdminAPI: migrate_from_db endpoint
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
|
@ -172,7 +172,7 @@
|
||||||
"application/ld+json" => ["activity+json"]
|
"application/ld+json" => ["activity+json"]
|
||||||
}
|
}
|
||||||
|
|
||||||
config :tesla, adapter: Tesla.Adapter.Hackney
|
config :tesla, adapter: Tesla.Adapter.Gun
|
||||||
|
|
||||||
# Configures http settings, upstream proxy etc.
|
# Configures http settings, upstream proxy etc.
|
||||||
config :pleroma, :http,
|
config :pleroma, :http,
|
||||||
|
@ -205,6 +205,7 @@
|
||||||
registrations_open: true,
|
registrations_open: true,
|
||||||
invites_enabled: false,
|
invites_enabled: false,
|
||||||
account_activation_required: false,
|
account_activation_required: false,
|
||||||
|
account_approval_required: false,
|
||||||
federating: true,
|
federating: true,
|
||||||
federation_incoming_replies_max_depth: 100,
|
federation_incoming_replies_max_depth: 100,
|
||||||
federation_reachability_timeout_days: 7,
|
federation_reachability_timeout_days: 7,
|
||||||
|
@ -225,8 +226,6 @@
|
||||||
autofollowed_nicknames: [],
|
autofollowed_nicknames: [],
|
||||||
max_pinned_statuses: 1,
|
max_pinned_statuses: 1,
|
||||||
attachment_links: false,
|
attachment_links: false,
|
||||||
welcome_user_nickname: nil,
|
|
||||||
welcome_message: nil,
|
|
||||||
max_report_comment_size: 1000,
|
max_report_comment_size: 1000,
|
||||||
safe_dm_mentions: false,
|
safe_dm_mentions: false,
|
||||||
healthcheck: false,
|
healthcheck: false,
|
||||||
|
@ -239,6 +238,7 @@
|
||||||
max_remote_account_fields: 20,
|
max_remote_account_fields: 20,
|
||||||
account_field_name_length: 512,
|
account_field_name_length: 512,
|
||||||
account_field_value_length: 2048,
|
account_field_value_length: 2048,
|
||||||
|
registration_reason_length: 500,
|
||||||
external_user_synchronization: true,
|
external_user_synchronization: true,
|
||||||
extended_nickname_format: true,
|
extended_nickname_format: true,
|
||||||
cleanup_attachments: false,
|
cleanup_attachments: false,
|
||||||
|
@ -254,6 +254,20 @@
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
config :pleroma, :welcome,
|
||||||
|
direct_message: [
|
||||||
|
enabled: false,
|
||||||
|
sender_nickname: nil,
|
||||||
|
message: nil
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
enabled: false,
|
||||||
|
sender: nil,
|
||||||
|
subject: "Welcome to <%= instance_name %>",
|
||||||
|
html: "Welcome to <%= instance_name %>",
|
||||||
|
text: "Welcome to <%= instance_name %>"
|
||||||
|
]
|
||||||
|
|
||||||
config :pleroma, :feed,
|
config :pleroma, :feed,
|
||||||
post_title: %{
|
post_title: %{
|
||||||
max_length: 100,
|
max_length: 100,
|
||||||
|
@ -512,6 +526,7 @@
|
||||||
attachments_cleanup: 5,
|
attachments_cleanup: 5,
|
||||||
new_users_digest: 1
|
new_users_digest: 1
|
||||||
],
|
],
|
||||||
|
plugins: [Oban.Plugins.Pruner],
|
||||||
crontab: [
|
crontab: [
|
||||||
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
|
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
|
||||||
{"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
|
{"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
|
||||||
|
@ -526,16 +541,14 @@
|
||||||
federator_outgoing: 5
|
federator_outgoing: 5
|
||||||
]
|
]
|
||||||
|
|
||||||
config :auto_linker,
|
config :pleroma, Pleroma.Formatter,
|
||||||
opts: [
|
class: false,
|
||||||
extra: true,
|
rel: "ugc",
|
||||||
# TODO: Set to :no_scheme when it works properly
|
new_window: false,
|
||||||
validate_tld: true,
|
truncate: false,
|
||||||
class: false,
|
strip_prefix: false,
|
||||||
strip_prefix: false,
|
extra: true,
|
||||||
new_window: false,
|
validate_tld: :no_scheme
|
||||||
rel: "ugc"
|
|
||||||
]
|
|
||||||
|
|
||||||
config :pleroma, :ldap,
|
config :pleroma, :ldap,
|
||||||
enabled: System.get_env("LDAP_ENABLED") == "true",
|
enabled: System.get_env("LDAP_ENABLED") == "true",
|
||||||
|
@ -634,6 +647,16 @@
|
||||||
|
|
||||||
config :pleroma, :static_fe, enabled: false
|
config :pleroma, :static_fe, enabled: false
|
||||||
|
|
||||||
|
# Example of frontend configuration
|
||||||
|
# This example will make us serve the primary frontend from the
|
||||||
|
# frontends directory within your `:pleroma, :instance, static_dir`.
|
||||||
|
# e.g., instance/static/frontends/pleroma/develop/
|
||||||
|
#
|
||||||
|
# With no frontend configuration, the bundled files from the `static` directory will
|
||||||
|
# be used.
|
||||||
|
#
|
||||||
|
# config :pleroma, :frontends, primary: %{"name" => "pleroma", "ref" => "develop"}
|
||||||
|
|
||||||
config :pleroma, :web_cache_ttl,
|
config :pleroma, :web_cache_ttl,
|
||||||
activity_pub: nil,
|
activity_pub: nil,
|
||||||
activity_pub_question: 30_000
|
activity_pub_question: 30_000
|
||||||
|
@ -647,32 +670,30 @@
|
||||||
prepare: :unnamed
|
prepare: :unnamed
|
||||||
|
|
||||||
config :pleroma, :connections_pool,
|
config :pleroma, :connections_pool,
|
||||||
checkin_timeout: 250,
|
reclaim_multiplier: 0.1,
|
||||||
|
connection_acquisition_wait: 250,
|
||||||
|
connection_acquisition_retries: 5,
|
||||||
max_connections: 250,
|
max_connections: 250,
|
||||||
retry: 1,
|
max_idle_time: 30_000,
|
||||||
retry_timeout: 1000,
|
retry: 0,
|
||||||
await_up_timeout: 5_000
|
await_up_timeout: 5_000
|
||||||
|
|
||||||
config :pleroma, :pools,
|
config :pleroma, :pools,
|
||||||
federation: [
|
federation: [
|
||||||
size: 50,
|
size: 50,
|
||||||
max_overflow: 10,
|
max_waiting: 10
|
||||||
timeout: 150_000
|
|
||||||
],
|
],
|
||||||
media: [
|
media: [
|
||||||
size: 50,
|
size: 50,
|
||||||
max_overflow: 10,
|
max_waiting: 10
|
||||||
timeout: 150_000
|
|
||||||
],
|
],
|
||||||
upload: [
|
upload: [
|
||||||
size: 25,
|
size: 25,
|
||||||
max_overflow: 5,
|
max_waiting: 5
|
||||||
timeout: 300_000
|
|
||||||
],
|
],
|
||||||
default: [
|
default: [
|
||||||
size: 10,
|
size: 10,
|
||||||
max_overflow: 2,
|
max_waiting: 2
|
||||||
timeout: 10_000
|
|
||||||
]
|
]
|
||||||
|
|
||||||
config :pleroma, :hackney_pools,
|
config :pleroma, :hackney_pools,
|
||||||
|
|
|
@ -661,6 +661,11 @@
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Require users to confirm their emails before signing in"
|
description: "Require users to confirm their emails before signing in"
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
key: :account_approval_required,
|
||||||
|
type: :boolean,
|
||||||
|
description: "Require users to be manually approved by an admin before signing in"
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
key: :federating,
|
key: :federating,
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
|
@ -778,23 +783,6 @@
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Enable to automatically add attachment link text to statuses"
|
description: "Enable to automatically add attachment link text to statuses"
|
||||||
},
|
},
|
||||||
%{
|
|
||||||
key: :welcome_message,
|
|
||||||
type: :string,
|
|
||||||
description:
|
|
||||||
"A message that will be sent to a newly registered users as a direct message",
|
|
||||||
suggestions: [
|
|
||||||
"Hi, @username! Welcome on board!"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :welcome_user_nickname,
|
|
||||||
type: :string,
|
|
||||||
description: "The nickname of the local user that sends the welcome message",
|
|
||||||
suggestions: [
|
|
||||||
"lain"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
%{
|
%{
|
||||||
key: :max_report_comment_size,
|
key: :max_report_comment_size,
|
||||||
type: :integer,
|
type: :integer,
|
||||||
|
@ -891,6 +879,14 @@
|
||||||
2048
|
2048
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
key: :registration_reason_length,
|
||||||
|
type: :integer,
|
||||||
|
description: "Maximum registration reason length. Default: 500.",
|
||||||
|
suggestions: [
|
||||||
|
500
|
||||||
|
]
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
key: :external_user_synchronization,
|
key: :external_user_synchronization,
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
|
@ -962,6 +958,84 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
group: :welcome,
|
||||||
|
type: :group,
|
||||||
|
description: "Welcome messages settings",
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
group: :direct_message,
|
||||||
|
type: :group,
|
||||||
|
descpiption: "Direct message settings",
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :enabled,
|
||||||
|
type: :boolean,
|
||||||
|
description: "Enables sends direct message for new user after registration"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :message,
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"A message that will be sent to a newly registered users as a direct message",
|
||||||
|
suggestions: [
|
||||||
|
"Hi, @username! Welcome on board!"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :sender_nickname,
|
||||||
|
type: :string,
|
||||||
|
description: "The nickname of the local user that sends the welcome message",
|
||||||
|
suggestions: [
|
||||||
|
"lain"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
group: :email,
|
||||||
|
type: :group,
|
||||||
|
descpiption: "Email message settings",
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :enabled,
|
||||||
|
type: :boolean,
|
||||||
|
description: "Enables sends direct message for new user after registration"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :sender,
|
||||||
|
type: [:string, :tuple],
|
||||||
|
description:
|
||||||
|
"The email address or tuple with `{nickname, email}` that will use as sender to the welcome email.",
|
||||||
|
suggestions: [
|
||||||
|
{"Pleroma App", "welcome@pleroma.app"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :subject,
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"The subject of welcome email. Can be use EEX template with `user` and `instance_name` variables.",
|
||||||
|
suggestions: ["Welcome to <%= instance_name%>"]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :html,
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"The html content of welcome email. Can be use EEX template with `user` and `instance_name` variables.",
|
||||||
|
suggestions: ["<h1>Hello <%= user.name%>. Welcome to <%= instance_name%></h1>"]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :text,
|
||||||
|
type: :string,
|
||||||
|
description:
|
||||||
|
"The text content of welcome email. Can be use EEX template with `user` and `instance_name` variables.",
|
||||||
|
suggestions: ["Hello <%= user.name%>. \n Welcome to <%= instance_name%>\n"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
group: :logger,
|
group: :logger,
|
||||||
type: :group,
|
type: :group,
|
||||||
|
@ -1426,6 +1500,7 @@
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :mrf_simple,
|
key: :mrf_simple,
|
||||||
tab: :mrf,
|
tab: :mrf,
|
||||||
|
related_policy: "Pleroma.Web.ActivityPub.MRF.SimplePolicy",
|
||||||
label: "MRF Simple",
|
label: "MRF Simple",
|
||||||
type: :group,
|
type: :group,
|
||||||
description: "Simple ingress policies",
|
description: "Simple ingress policies",
|
||||||
|
@ -1492,6 +1567,7 @@
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :mrf_activity_expiration,
|
key: :mrf_activity_expiration,
|
||||||
tab: :mrf,
|
tab: :mrf,
|
||||||
|
related_policy: "Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy",
|
||||||
label: "MRF Activity Expiration Policy",
|
label: "MRF Activity Expiration Policy",
|
||||||
type: :group,
|
type: :group,
|
||||||
description: "Adds automatic expiration to all local activities",
|
description: "Adds automatic expiration to all local activities",
|
||||||
|
@ -1508,6 +1584,7 @@
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :mrf_subchain,
|
key: :mrf_subchain,
|
||||||
tab: :mrf,
|
tab: :mrf,
|
||||||
|
related_policy: "Pleroma.Web.ActivityPub.MRF.SubchainPolicy",
|
||||||
label: "MRF Subchain",
|
label: "MRF Subchain",
|
||||||
type: :group,
|
type: :group,
|
||||||
description:
|
description:
|
||||||
|
@ -1530,6 +1607,7 @@
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :mrf_rejectnonpublic,
|
key: :mrf_rejectnonpublic,
|
||||||
tab: :mrf,
|
tab: :mrf,
|
||||||
|
related_policy: "Pleroma.Web.ActivityPub.MRF.RejectNonPublic",
|
||||||
description: "RejectNonPublic drops posts with non-public visibility settings.",
|
description: "RejectNonPublic drops posts with non-public visibility settings.",
|
||||||
label: "MRF Reject Non Public",
|
label: "MRF Reject Non Public",
|
||||||
type: :group,
|
type: :group,
|
||||||
|
@ -1551,6 +1629,7 @@
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :mrf_hellthread,
|
key: :mrf_hellthread,
|
||||||
tab: :mrf,
|
tab: :mrf,
|
||||||
|
related_policy: "Pleroma.Web.ActivityPub.MRF.HellthreadPolicy",
|
||||||
label: "MRF Hellthread",
|
label: "MRF Hellthread",
|
||||||
type: :group,
|
type: :group,
|
||||||
description: "Block messages with excessive user mentions",
|
description: "Block messages with excessive user mentions",
|
||||||
|
@ -1576,6 +1655,7 @@
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :mrf_keyword,
|
key: :mrf_keyword,
|
||||||
tab: :mrf,
|
tab: :mrf,
|
||||||
|
related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy",
|
||||||
label: "MRF Keyword",
|
label: "MRF Keyword",
|
||||||
type: :group,
|
type: :group,
|
||||||
description: "Reject or Word-Replace messages with a keyword or regex",
|
description: "Reject or Word-Replace messages with a keyword or regex",
|
||||||
|
@ -1607,6 +1687,7 @@
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :mrf_mention,
|
key: :mrf_mention,
|
||||||
tab: :mrf,
|
tab: :mrf,
|
||||||
|
related_policy: "Pleroma.Web.ActivityPub.MRF.MentionPolicy",
|
||||||
label: "MRF Mention",
|
label: "MRF Mention",
|
||||||
type: :group,
|
type: :group,
|
||||||
description: "Block messages which mention a specific user",
|
description: "Block messages which mention a specific user",
|
||||||
|
@ -1623,6 +1704,7 @@
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :mrf_vocabulary,
|
key: :mrf_vocabulary,
|
||||||
tab: :mrf,
|
tab: :mrf,
|
||||||
|
related_policy: "Pleroma.Web.ActivityPub.MRF.VocabularyPolicy",
|
||||||
label: "MRF Vocabulary",
|
label: "MRF Vocabulary",
|
||||||
type: :group,
|
type: :group,
|
||||||
description: "Filter messages which belong to certain activity vocabularies",
|
description: "Filter messages which belong to certain activity vocabularies",
|
||||||
|
@ -1646,6 +1728,8 @@
|
||||||
# %{
|
# %{
|
||||||
# group: :pleroma,
|
# group: :pleroma,
|
||||||
# key: :mrf_user_allowlist,
|
# key: :mrf_user_allowlist,
|
||||||
|
# tab: :mrf,
|
||||||
|
# related_policy: "Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy",
|
||||||
# type: :map,
|
# type: :map,
|
||||||
# description:
|
# description:
|
||||||
# "The keys in this section are the domain names that the policy should apply to." <>
|
# "The keys in this section are the domain names that the policy should apply to." <>
|
||||||
|
@ -2216,45 +2300,53 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
group: :auto_linker,
|
group: :pleroma,
|
||||||
key: :opts,
|
key: Pleroma.Formatter,
|
||||||
label: "Auto Linker",
|
label: "Auto Linker",
|
||||||
type: :group,
|
type: :group,
|
||||||
description: "Configuration for the auto_linker library",
|
description:
|
||||||
|
"Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs.",
|
||||||
children: [
|
children: [
|
||||||
%{
|
%{
|
||||||
key: :class,
|
key: :class,
|
||||||
type: [:string, false],
|
type: [:string, :boolean],
|
||||||
description: "Specify the class to be added to the generated link. Disable to clear.",
|
description: "Specify the class to be added to the generated link. Disable to clear.",
|
||||||
suggestions: ["auto-linker", false]
|
suggestions: ["auto-linker", false]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :rel,
|
key: :rel,
|
||||||
type: [:string, false],
|
type: [:string, :boolean],
|
||||||
description: "Override the rel attribute. Disable to clear.",
|
description: "Override the rel attribute. Disable to clear.",
|
||||||
suggestions: ["ugc", "noopener noreferrer", false]
|
suggestions: ["ugc", "noopener noreferrer", false]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :new_window,
|
key: :new_window,
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Link URLs will open in new window/tab"
|
description: "Link URLs will open in a new window/tab."
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :truncate,
|
key: :truncate,
|
||||||
type: [:integer, false],
|
type: [:integer, :boolean],
|
||||||
description:
|
description:
|
||||||
"Set to a number to truncate URLs longer then the number. Truncated URLs will end in `..`",
|
"Set to a number to truncate URLs longer than the number. Truncated URLs will end in `...`",
|
||||||
suggestions: [15, false]
|
suggestions: [15, false]
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :strip_prefix,
|
key: :strip_prefix,
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Strip the scheme prefix"
|
description: "Strip the scheme prefix."
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
key: :extra,
|
key: :extra,
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Link URLs with rarely used schemes (magnet, ipfs, irc, etc.)"
|
description: "Link URLs with rarely used schemes (magnet, ipfs, irc, etc.)"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :validate_tld,
|
||||||
|
type: [:atom, :boolean],
|
||||||
|
description:
|
||||||
|
"Set to false to disable TLD validation for URLs/emails. Can be set to :no_scheme to validate TLDs only for URLs without a scheme (e.g `example.com` will be validated, but `http://example.loki` won't)",
|
||||||
|
suggestions: [:no_scheme, true]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -2902,8 +2994,9 @@
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
tab: :mrf,
|
|
||||||
key: :mrf_normalize_markup,
|
key: :mrf_normalize_markup,
|
||||||
|
tab: :mrf,
|
||||||
|
related_policy: "Pleroma.Web.ActivityPub.MRF.NormalizeMarkup",
|
||||||
label: "MRF Normalize Markup",
|
label: "MRF Normalize Markup",
|
||||||
description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
|
description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
|
||||||
type: :group,
|
type: :group,
|
||||||
|
@ -3098,8 +3191,9 @@
|
||||||
%{
|
%{
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
key: :mrf_object_age,
|
key: :mrf_object_age,
|
||||||
label: "MRF Object Age",
|
|
||||||
tab: :mrf,
|
tab: :mrf,
|
||||||
|
related_policy: "Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy",
|
||||||
|
label: "MRF Object Age",
|
||||||
type: :group,
|
type: :group,
|
||||||
description:
|
description:
|
||||||
"Rejects or delists posts based on their timestamp deviance from your server's clock.",
|
"Rejects or delists posts based on their timestamp deviance from your server's clock.",
|
||||||
|
@ -3161,36 +3255,37 @@
|
||||||
description: "Advanced settings for `gun` connections pool",
|
description: "Advanced settings for `gun` connections pool",
|
||||||
children: [
|
children: [
|
||||||
%{
|
%{
|
||||||
key: :checkin_timeout,
|
key: :connection_acquisition_wait,
|
||||||
type: :integer,
|
type: :integer,
|
||||||
description: "Timeout to checkin connection from pool. Default: 250ms.",
|
description:
|
||||||
|
"Timeout to acquire a connection from pool.The total max time is this value multiplied by the number of retries. Default: 250ms.",
|
||||||
suggestions: [250]
|
suggestions: [250]
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
key: :connection_acquisition_retries,
|
||||||
|
type: :integer,
|
||||||
|
description:
|
||||||
|
"Number of attempts to acquire the connection from the pool if it is overloaded. Default: 5",
|
||||||
|
suggestions: [5]
|
||||||
|
},
|
||||||
%{
|
%{
|
||||||
key: :max_connections,
|
key: :max_connections,
|
||||||
type: :integer,
|
type: :integer,
|
||||||
description: "Maximum number of connections in the pool. Default: 250 connections.",
|
description: "Maximum number of connections in the pool. Default: 250 connections.",
|
||||||
suggestions: [250]
|
suggestions: [250]
|
||||||
},
|
},
|
||||||
%{
|
|
||||||
key: :retry,
|
|
||||||
type: :integer,
|
|
||||||
description:
|
|
||||||
"Number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.",
|
|
||||||
suggestions: [1]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :retry_timeout,
|
|
||||||
type: :integer,
|
|
||||||
description:
|
|
||||||
"Time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.",
|
|
||||||
suggestions: [1000]
|
|
||||||
},
|
|
||||||
%{
|
%{
|
||||||
key: :await_up_timeout,
|
key: :await_up_timeout,
|
||||||
type: :integer,
|
type: :integer,
|
||||||
description: "Timeout while `gun` will wait until connection is up. Default: 5000ms.",
|
description: "Timeout while `gun` will wait until connection is up. Default: 5000ms.",
|
||||||
suggestions: [5000]
|
suggestions: [5000]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :reclaim_multiplier,
|
||||||
|
type: :integer,
|
||||||
|
description:
|
||||||
|
"Multiplier for the number of idle connection to be reclaimed if the pool is full. For example if the pool maxes out at 250 connections and this setting is set to 0.3, the pool will reclaim at most 75 idle connections if it's overloaded. Default: 0.1",
|
||||||
|
suggestions: [0.1]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3199,108 +3294,29 @@
|
||||||
key: :pools,
|
key: :pools,
|
||||||
type: :group,
|
type: :group,
|
||||||
description: "Advanced settings for `gun` workers pools",
|
description: "Advanced settings for `gun` workers pools",
|
||||||
children: [
|
children:
|
||||||
%{
|
Enum.map([:federation, :media, :upload, :default], fn pool_name ->
|
||||||
key: :federation,
|
%{
|
||||||
type: :keyword,
|
key: pool_name,
|
||||||
description: "Settings for federation pool.",
|
type: :keyword,
|
||||||
children: [
|
description: "Settings for #{pool_name} pool.",
|
||||||
%{
|
children: [
|
||||||
key: :size,
|
%{
|
||||||
type: :integer,
|
key: :size,
|
||||||
description: "Number workers in the pool.",
|
type: :integer,
|
||||||
suggestions: [50]
|
description: "Maximum number of concurrent requests in the pool.",
|
||||||
},
|
suggestions: [50]
|
||||||
%{
|
},
|
||||||
key: :max_overflow,
|
%{
|
||||||
type: :integer,
|
key: :max_waiting,
|
||||||
description: "Number of additional workers if pool is under load.",
|
type: :integer,
|
||||||
suggestions: [10]
|
description:
|
||||||
},
|
"Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made",
|
||||||
%{
|
suggestions: [10]
|
||||||
key: :timeout,
|
}
|
||||||
type: :integer,
|
]
|
||||||
description: "Timeout while `gun` will wait for response.",
|
}
|
||||||
suggestions: [150_000]
|
end)
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :media,
|
|
||||||
type: :keyword,
|
|
||||||
description: "Settings for media pool.",
|
|
||||||
children: [
|
|
||||||
%{
|
|
||||||
key: :size,
|
|
||||||
type: :integer,
|
|
||||||
description: "Number workers in the pool.",
|
|
||||||
suggestions: [50]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :max_overflow,
|
|
||||||
type: :integer,
|
|
||||||
description: "Number of additional workers if pool is under load.",
|
|
||||||
suggestions: [10]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :timeout,
|
|
||||||
type: :integer,
|
|
||||||
description: "Timeout while `gun` will wait for response.",
|
|
||||||
suggestions: [150_000]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :upload,
|
|
||||||
type: :keyword,
|
|
||||||
description: "Settings for upload pool.",
|
|
||||||
children: [
|
|
||||||
%{
|
|
||||||
key: :size,
|
|
||||||
type: :integer,
|
|
||||||
description: "Number workers in the pool.",
|
|
||||||
suggestions: [25]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :max_overflow,
|
|
||||||
type: :integer,
|
|
||||||
description: "Number of additional workers if pool is under load.",
|
|
||||||
suggestions: [5]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :timeout,
|
|
||||||
type: :integer,
|
|
||||||
description: "Timeout while `gun` will wait for response.",
|
|
||||||
suggestions: [300_000]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :default,
|
|
||||||
type: :keyword,
|
|
||||||
description: "Settings for default pool.",
|
|
||||||
children: [
|
|
||||||
%{
|
|
||||||
key: :size,
|
|
||||||
type: :integer,
|
|
||||||
description: "Number workers in the pool.",
|
|
||||||
suggestions: [10]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :max_overflow,
|
|
||||||
type: :integer,
|
|
||||||
description: "Number of additional workers if pool is under load.",
|
|
||||||
suggestions: [2]
|
|
||||||
},
|
|
||||||
%{
|
|
||||||
key: :timeout,
|
|
||||||
type: :integer,
|
|
||||||
description: "Timeout while `gun` will wait for response.",
|
|
||||||
suggestions: [10_000]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
group: :pleroma,
|
group: :pleroma,
|
||||||
|
@ -3478,5 +3494,30 @@
|
||||||
suggestions: ["s3.eu-central-1.amazonaws.com"]
|
suggestions: ["s3.eu-central-1.amazonaws.com"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
group: :pleroma,
|
||||||
|
key: :frontends,
|
||||||
|
type: :group,
|
||||||
|
description: "Installed frontends management",
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: :primary,
|
||||||
|
type: :map,
|
||||||
|
description: "Primary frontend, the one that is served for all pages by default",
|
||||||
|
children: [
|
||||||
|
%{
|
||||||
|
key: "name",
|
||||||
|
type: :string,
|
||||||
|
description: "Name of the installed primary frontend"
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: "ref",
|
||||||
|
type: :string,
|
||||||
|
description: "reference of the installed primary frontend to be used"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -118,6 +118,8 @@
|
||||||
streaming_enabled: true,
|
streaming_enabled: true,
|
||||||
public_endpoint: nil
|
public_endpoint: nil
|
||||||
|
|
||||||
|
config :tzdata, :autoupdate, :disabled
|
||||||
|
|
||||||
if File.exists?("./config/test.secret.exs") do
|
if File.exists?("./config/test.secret.exs") do
|
||||||
import_config "test.secret.exs"
|
import_config "test.secret.exs"
|
||||||
else
|
else
|
||||||
|
|
|
@ -19,6 +19,7 @@ Configuration options:
|
||||||
- `local`: only local users
|
- `local`: only local users
|
||||||
- `external`: only external users
|
- `external`: only external users
|
||||||
- `active`: only active users
|
- `active`: only active users
|
||||||
|
- `need_approval`: only unapproved users
|
||||||
- `deactivated`: only deactivated users
|
- `deactivated`: only deactivated users
|
||||||
- `is_admin`: users with admin role
|
- `is_admin`: users with admin role
|
||||||
- `is_moderator`: users with moderator role
|
- `is_moderator`: users with moderator role
|
||||||
|
@ -46,7 +47,10 @@ Configuration options:
|
||||||
"local": bool,
|
"local": bool,
|
||||||
"tags": array,
|
"tags": array,
|
||||||
"avatar": string,
|
"avatar": string,
|
||||||
"display_name": string
|
"display_name": string,
|
||||||
|
"confirmation_pending": bool,
|
||||||
|
"approval_pending": bool,
|
||||||
|
"registration_reason": string,
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
|
@ -242,6 +246,24 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `PATCH /api/pleroma/admin/users/approve`
|
||||||
|
|
||||||
|
### Approve user
|
||||||
|
|
||||||
|
- Params:
|
||||||
|
- `nicknames`: nicknames array
|
||||||
|
- Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
// user object
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## `GET /api/pleroma/admin/users/:nickname_or_id`
|
## `GET /api/pleroma/admin/users/:nickname_or_id`
|
||||||
|
|
||||||
### Retrive the details of a user
|
### Retrive the details of a user
|
||||||
|
|
|
@ -236,6 +236,7 @@ Has theses additional parameters (which are the same as in Pleroma-API):
|
||||||
- `pleroma.metadata.features`: A list of supported features
|
- `pleroma.metadata.features`: A list of supported features
|
||||||
- `pleroma.metadata.federation`: The federation restrictions of this instance
|
- `pleroma.metadata.federation`: The federation restrictions of this instance
|
||||||
- `pleroma.metadata.fields_limits`: A list of values detailing the length and count limitation for various instance-configurable fields.
|
- `pleroma.metadata.fields_limits`: A list of values detailing the length and count limitation for various instance-configurable fields.
|
||||||
|
- `pleroma.metadata.post_formats`: A list of the allowed post format types
|
||||||
- `vapid_public_key`: The public key needed for push messages
|
- `vapid_public_key`: The public key needed for push messages
|
||||||
|
|
||||||
## Markers
|
## Markers
|
||||||
|
|
|
@ -287,11 +287,8 @@ See [Admin-API](admin_api.md)
|
||||||
* Method `PUT`
|
* Method `PUT`
|
||||||
* Authentication: required
|
* Authentication: required
|
||||||
* Params:
|
* Params:
|
||||||
* `followers`: BOOLEAN field, receives notifications from followers
|
* `block_from_strangers`: BOOLEAN field, blocks notifications from accounts you do not follow
|
||||||
* `follows`: BOOLEAN field, receives notifications from people the user follows
|
* `hide_notification_contents`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification.
|
||||||
* `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"}`
|
* Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}`
|
||||||
|
|
||||||
## `/api/pleroma/healthcheck`
|
## `/api/pleroma/healthcheck`
|
||||||
|
|
9
docs/administration/CLI_tasks/release_environments.md
Normal file
9
docs/administration/CLI_tasks/release_environments.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Generate release environment file
|
||||||
|
|
||||||
|
```sh tab="OTP"
|
||||||
|
./bin/pleroma_ctl release_env gen
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh tab="From Source"
|
||||||
|
mix pleroma.release_env gen
|
||||||
|
```
|
|
@ -33,6 +33,7 @@ To add configuration to your config file, you can copy it from the base config.
|
||||||
* `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
|
* `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
|
||||||
* `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
|
* `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
|
||||||
* `account_activation_required`: Require users to confirm their emails before signing in.
|
* `account_activation_required`: Require users to confirm their emails before signing in.
|
||||||
|
* `account_approval_required`: Require users to be manually approved by an admin before signing in.
|
||||||
* `federating`: Enable federation with other instances.
|
* `federating`: Enable federation with other instances.
|
||||||
* `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
|
* `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
|
||||||
* `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
|
* `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
|
||||||
|
@ -46,8 +47,6 @@ To add configuration to your config file, you can copy it from the base config.
|
||||||
* `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature.
|
* `max_pinned_statuses`: The maximum number of pinned statuses. `0` will disable the feature.
|
||||||
* `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow.
|
* `autofollowed_nicknames`: Set to nicknames of (local) users that every new user should automatically follow.
|
||||||
* `attachment_links`: Set to true to enable automatically adding attachment link text to statuses.
|
* `attachment_links`: Set to true to enable automatically adding attachment link text to statuses.
|
||||||
* `welcome_message`: A message that will be send to a newly registered users as a direct message.
|
|
||||||
* `welcome_user_nickname`: The nickname of the local user that sends the welcome message.
|
|
||||||
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`).
|
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`).
|
||||||
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). Default: `false`.
|
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). Default: `false`.
|
||||||
* `healthcheck`: If set to true, system data will be shown on ``/api/pleroma/healthcheck``.
|
* `healthcheck`: If set to true, system data will be shown on ``/api/pleroma/healthcheck``.
|
||||||
|
@ -60,9 +59,40 @@ To add configuration to your config file, you can copy it from the base config.
|
||||||
* `max_remote_account_fields`: The maximum number of custom fields in the remote user profile (default: `20`).
|
* `max_remote_account_fields`: The maximum number of custom fields in the remote user profile (default: `20`).
|
||||||
* `account_field_name_length`: An account field name maximum length (default: `512`).
|
* `account_field_name_length`: An account field name maximum length (default: `512`).
|
||||||
* `account_field_value_length`: An account field value maximum length (default: `2048`).
|
* `account_field_value_length`: An account field value maximum length (default: `2048`).
|
||||||
|
* `registration_reason_length`: Maximum registration reason length (default: `500`).
|
||||||
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
|
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
|
||||||
* `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances.
|
* `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances.
|
||||||
|
|
||||||
|
## Welcome
|
||||||
|
* `direct_message`: - welcome message sent as a direct message.
|
||||||
|
* `enabled`: Enables the send a direct message to a newly registered user. Defaults to `false`.
|
||||||
|
* `sender_nickname`: The nickname of the local user that sends the welcome message.
|
||||||
|
* `message`: A message that will be send to a newly registered users as a direct message.
|
||||||
|
* `email`: - welcome message sent as a email.
|
||||||
|
* `enabled`: Enables the send a welcome email to a newly registered user. Defaults to `false`.
|
||||||
|
* `sender`: The email address or tuple with `{nickname, email}` that will use as sender to the welcome email.
|
||||||
|
* `subject`: A subject of welcome email.
|
||||||
|
* `html`: A html that will be send to a newly registered users as a email.
|
||||||
|
* `text`: A text that will be send to a newly registered users as a email.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
config :pleroma, :welcome,
|
||||||
|
direct_message: [
|
||||||
|
enabled: true,
|
||||||
|
sender_nickname: "lain",
|
||||||
|
message: "Hi, @username! Welcome on board!"
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
enabled: true,
|
||||||
|
sender: {"Pleroma App", "welcome@pleroma.app"},
|
||||||
|
subject: "Welcome to <%= instance_name %>",
|
||||||
|
html: "Welcome to <%= instance_name %>",
|
||||||
|
text: "Welcome to <%= instance_name %>"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
## Message rewrite facility
|
## Message rewrite facility
|
||||||
|
|
||||||
### :mrf
|
### :mrf
|
||||||
|
@ -448,36 +478,32 @@ For each pool, the options are:
|
||||||
|
|
||||||
*For `gun` adapter*
|
*For `gun` adapter*
|
||||||
|
|
||||||
Advanced settings for connections pool. Pool with opened connections. These connections can be reused in worker pools.
|
Settings for HTTP connection pool.
|
||||||
|
|
||||||
For big instances it's recommended to increase `config :pleroma, :connections_pool, max_connections: 500` up to 500-1000.
|
* `:connection_acquisition_wait` - Timeout to acquire a connection from pool.The total max time is this value multiplied by the number of retries.
|
||||||
It will increase memory usage, but federation would work faster.
|
* `connection_acquisition_retries` - Number of attempts to acquire the connection from the pool if it is overloaded. Each attempt is timed `:connection_acquisition_wait` apart.
|
||||||
|
* `:max_connections` - Maximum number of connections in the pool.
|
||||||
* `:checkin_timeout` - timeout to checkin connection from pool. Default: 250ms.
|
* `:await_up_timeout` - Timeout to connect to the host.
|
||||||
* `:max_connections` - maximum number of connections in the pool. Default: 250 connections.
|
* `:reclaim_multiplier` - Multiplied by `:max_connections` this will be the maximum number of idle connections that will be reclaimed in case the pool is overloaded.
|
||||||
* `:retry` - number of retries, while `gun` will try to reconnect if connection goes down. Default: 1.
|
|
||||||
* `:retry_timeout` - time between retries when `gun` will try to reconnect in milliseconds. Default: 1000ms.
|
|
||||||
* `:await_up_timeout` - timeout while `gun` will wait until connection is up. Default: 5000ms.
|
|
||||||
|
|
||||||
### :pools
|
### :pools
|
||||||
|
|
||||||
*For `gun` adapter*
|
*For `gun` adapter*
|
||||||
|
|
||||||
Advanced settings for workers pools.
|
Settings for request pools. These pools are limited on top of `:connections_pool`.
|
||||||
|
|
||||||
There are four pools used:
|
There are four pools used:
|
||||||
|
|
||||||
* `:federation` for the federation jobs.
|
* `:federation` for the federation jobs. You may want this pool's max_connections to be at least equal to the number of federator jobs + retry queue jobs.
|
||||||
You may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs.
|
* `:media` - for rich media, media proxy.
|
||||||
* `:media` for rich media, media proxy
|
* `:upload` - for proxying media when a remote uploader is used and `proxy_remote: true`.
|
||||||
* `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`)
|
* `:default` - for other requests.
|
||||||
* `:default` for other requests
|
|
||||||
|
|
||||||
For each pool, the options are:
|
For each pool, the options are:
|
||||||
|
|
||||||
* `:size` - how much workers the pool can hold
|
* `:size` - limit to how much requests can be concurrently executed.
|
||||||
* `:timeout` - timeout while `gun` will wait for response
|
* `:timeout` - timeout while `gun` will wait for response
|
||||||
* `:max_overflow` - additional workers if pool is under load
|
* `:max_waiting` - limit to how much requests can be waiting for others to finish, after this is reached, subsequent requests will be dropped.
|
||||||
|
|
||||||
## Captcha
|
## Captcha
|
||||||
|
|
||||||
|
@ -938,30 +964,29 @@ Configure OAuth 2 provider capabilities:
|
||||||
### :uri_schemes
|
### :uri_schemes
|
||||||
* `valid_schemes`: List of the scheme part that is considered valid to be an URL.
|
* `valid_schemes`: List of the scheme part that is considered valid to be an URL.
|
||||||
|
|
||||||
### :auto_linker
|
### Pleroma.Formatter
|
||||||
|
|
||||||
Configuration for the `auto_linker` library:
|
Configuration for Pleroma's link formatter which parses mentions, hashtags, and URLs.
|
||||||
|
|
||||||
* `class: "auto-linker"` - specify the class to be added to the generated link. false to clear.
|
* `class` - specify the class to be added to the generated link (default: `false`)
|
||||||
* `rel: "noopener noreferrer"` - override the rel attribute. false to clear.
|
* `rel` - specify the rel attribute (default: `ugc`)
|
||||||
* `new_window: true` - set to false to remove `target='_blank'` attribute.
|
* `new_window` - adds `target="_blank"` attribute (default: `false`)
|
||||||
* `scheme: false` - Set to true to link urls with schema `http://google.com`.
|
* `truncate` - Set to a number to truncate URLs longer then the number. Truncated URLs will end in `...` (default: `false`)
|
||||||
* `truncate: false` - Set to a number to truncate urls longer then the number. Truncated urls will end in `..`.
|
* `strip_prefix` - Strip the scheme prefix (default: `false`)
|
||||||
* `strip_prefix: true` - Strip the scheme prefix.
|
* `extra` - link URLs with rarely used schemes (magnet, ipfs, irc, etc.) (default: `true`)
|
||||||
* `extra: false` - link urls with rarely used schemes (magnet, ipfs, irc, etc.).
|
* `validate_tld` - Set to false to disable TLD validation for URLs/emails. Can be set to :no_scheme to validate TLDs only for urls without a scheme (e.g `example.com` will be validated, but `http://example.loki` won't) (default: `:no_scheme`)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
config :auto_linker,
|
config :pleroma, Pleroma.Formatter,
|
||||||
opts: [
|
class: false,
|
||||||
scheme: true,
|
rel: "ugc",
|
||||||
extra: true,
|
new_window: false,
|
||||||
class: false,
|
truncate: false,
|
||||||
strip_prefix: false,
|
strip_prefix: false,
|
||||||
new_window: false,
|
extra: true,
|
||||||
rel: "ugc"
|
validate_tld: :no_scheme
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom Runtime Modules (`:modules`)
|
## Custom Runtime Modules (`:modules`)
|
||||||
|
@ -1023,3 +1048,25 @@ Note: setting `restrict_unauthenticated/timelines/local` to `true` has no practi
|
||||||
Control favicons for instances.
|
Control favicons for instances.
|
||||||
|
|
||||||
* `enabled`: Allow/disallow displaying and getting instances favicons
|
* `enabled`: Allow/disallow displaying and getting instances favicons
|
||||||
|
|
||||||
|
## Frontend management
|
||||||
|
|
||||||
|
Frontends in Pleroma are swappable - you can specify which one to use here.
|
||||||
|
|
||||||
|
For now, you can set a frontend with the key `primary` and the options of `name` and `ref`. This will then make Pleroma serve the frontend from a folder constructed by concatenating the instance static path, `frontends` and the name and ref.
|
||||||
|
|
||||||
|
The key `primary` refers to the frontend that will be served by default for general requests. In the future, other frontends like the admin frontend will also be configurable here.
|
||||||
|
|
||||||
|
If you don't set anything here, the bundled frontend will be used.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
config :pleroma, :frontends,
|
||||||
|
primary: %{
|
||||||
|
"name" => "pleroma",
|
||||||
|
"ref" => "stable"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would serve the frontend from the the folder at `$instance_static/frontends/pleroma/stable`. You have to copy the frontend into this folder yourself. You can choose the name and ref any way you like, but they will be used by mix tasks to automate installation in the future, the name referring to the project and the ref referring to a commit.
|
||||||
|
|
|
@ -121,6 +121,9 @@ chown -R pleroma /etc/pleroma
|
||||||
# Run the config generator
|
# Run the config generator
|
||||||
su pleroma -s $SHELL -lc "./bin/pleroma_ctl instance gen --output /etc/pleroma/config.exs --output-psql /tmp/setup_db.psql"
|
su pleroma -s $SHELL -lc "./bin/pleroma_ctl instance gen --output /etc/pleroma/config.exs --output-psql /tmp/setup_db.psql"
|
||||||
|
|
||||||
|
# Run the environment file generator.
|
||||||
|
su pleroma -s $SHELL -lc "./bin/pleroma_ctl release_env gen"
|
||||||
|
|
||||||
# Create the postgres database
|
# Create the postgres database
|
||||||
su postgres -s $SHELL -lc "psql -f /tmp/setup_db.psql"
|
su postgres -s $SHELL -lc "psql -f /tmp/setup_db.psql"
|
||||||
|
|
||||||
|
@ -131,7 +134,7 @@ su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate"
|
||||||
# su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate --migrations-path priv/repo/optional_migrations/rum_indexing/"
|
# su pleroma -s $SHELL -lc "./bin/pleroma_ctl migrate --migrations-path priv/repo/optional_migrations/rum_indexing/"
|
||||||
|
|
||||||
# Start the instance to verify that everything is working as expected
|
# Start the instance to verify that everything is working as expected
|
||||||
su pleroma -s $SHELL -lc "./bin/pleroma daemon"
|
su pleroma -s $SHELL -lc "export $(cat /opt/pleroma/config/pleroma.env); ./bin/pleroma daemon"
|
||||||
|
|
||||||
# Wait for about 20 seconds and query the instance endpoint, if it shows your uri, name and email correctly, you are configured correctly
|
# Wait for about 20 seconds and query the instance endpoint, if it shows your uri, name and email correctly, you are configured correctly
|
||||||
sleep 20 && curl http://localhost:4000/api/v1/instance
|
sleep 20 && curl http://localhost:4000/api/v1/instance
|
||||||
|
@ -200,6 +203,7 @@ rc-update add pleroma
|
||||||
# Copy the service into a proper directory
|
# Copy the service into a proper directory
|
||||||
cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service
|
cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service
|
||||||
|
|
||||||
|
|
||||||
# Start pleroma and enable it on boot
|
# Start pleroma and enable it on boot
|
||||||
systemctl start pleroma
|
systemctl start pleroma
|
||||||
systemctl enable pleroma
|
systemctl enable pleroma
|
||||||
|
@ -275,4 +279,3 @@ This will create an account withe the username of 'joeuser' with the email addre
|
||||||
## Questions
|
## Questions
|
||||||
|
|
||||||
Questions about the installation or didn’t it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**.
|
Questions about the installation or didn’t it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**.
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ pidfile="/var/run/pleroma.pid"
|
||||||
directory=/opt/pleroma
|
directory=/opt/pleroma
|
||||||
healthcheck_delay=60
|
healthcheck_delay=60
|
||||||
healthcheck_timer=30
|
healthcheck_timer=30
|
||||||
|
export $(cat /opt/pleroma/config/pleroma.env)
|
||||||
|
|
||||||
: ${pleroma_port:-4000}
|
: ${pleroma_port:-4000}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ Environment="MIX_ENV=prod"
|
||||||
Environment="HOME=/var/lib/pleroma"
|
Environment="HOME=/var/lib/pleroma"
|
||||||
; Path to the folder containing the Pleroma installation.
|
; Path to the folder containing the Pleroma installation.
|
||||||
WorkingDirectory=/opt/pleroma
|
WorkingDirectory=/opt/pleroma
|
||||||
|
; Path to the environment file. the file contains RELEASE_COOKIE and etc
|
||||||
|
EnvironmentFile=/opt/pleroma/config/pleroma.env
|
||||||
; Path to the Mix binary.
|
; Path to the Mix binary.
|
||||||
ExecStart=/usr/bin/mix phx.server
|
ExecStart=/usr/bin/mix phx.server
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,10 @@ def start_pleroma do
|
||||||
Application.put_env(:logger, :console, level: :debug)
|
Application.put_env(:logger, :console, level: :debug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
adapter = Application.get_env(:tesla, :adapter)
|
||||||
|
|
||||||
apps =
|
apps =
|
||||||
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do
|
if adapter == Tesla.Adapter.Gun do
|
||||||
[:gun | @apps]
|
[:gun | @apps]
|
||||||
else
|
else
|
||||||
[:hackney | @apps]
|
[:hackney | @apps]
|
||||||
|
@ -33,11 +35,14 @@ def start_pleroma do
|
||||||
|
|
||||||
Enum.each(apps, &Application.ensure_all_started/1)
|
Enum.each(apps, &Application.ensure_all_started/1)
|
||||||
|
|
||||||
children = [
|
children =
|
||||||
Pleroma.Repo,
|
[
|
||||||
{Pleroma.Config.TransferTask, false},
|
Pleroma.Repo,
|
||||||
Pleroma.Web.Endpoint
|
{Pleroma.Config.TransferTask, false},
|
||||||
]
|
Pleroma.Web.Endpoint,
|
||||||
|
{Oban, Pleroma.Config.get(Oban)}
|
||||||
|
] ++
|
||||||
|
http_children(adapter)
|
||||||
|
|
||||||
cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
|
cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
|
||||||
|
|
||||||
|
@ -115,4 +120,11 @@ def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0)
|
||||||
def escape_sh_path(path) do
|
def escape_sh_path(path) do
|
||||||
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
|
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp http_children(Tesla.Adapter.Gun) do
|
||||||
|
Pleroma.Gun.ConnectionPool.children() ++
|
||||||
|
[{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp http_children(_), do: []
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,8 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
> mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588" # set false only for parallel588 user
|
> mix pleroma.notification_settings --hide-notification-contents=false --nickname-users="parallel588" # set false only for parallel588 user
|
||||||
> mix pleroma.notification_settings --privacy-option=true # set true for all users
|
> mix pleroma.notification_settings --hide-notification-contents=true # set true for all users
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -19,16 +19,16 @@ def run(args) do
|
||||||
OptionParser.parse(
|
OptionParser.parse(
|
||||||
args,
|
args,
|
||||||
strict: [
|
strict: [
|
||||||
privacy_option: :boolean,
|
hide_notification_contents: :boolean,
|
||||||
email_users: :string,
|
email_users: :string,
|
||||||
nickname_users: :string
|
nickname_users: :string
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
privacy_option = Keyword.get(options, :privacy_option)
|
hide_notification_contents = Keyword.get(options, :hide_notification_contents)
|
||||||
|
|
||||||
if not is_nil(privacy_option) do
|
if not is_nil(hide_notification_contents) do
|
||||||
privacy_option
|
hide_notification_contents
|
||||||
|> build_query(options)
|
|> build_query(options)
|
||||||
|> Pleroma.Repo.update_all([])
|
|> Pleroma.Repo.update_all([])
|
||||||
end
|
end
|
||||||
|
@ -36,15 +36,15 @@ def run(args) do
|
||||||
shell_info("Done")
|
shell_info("Done")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_query(privacy_option, options) do
|
defp build_query(hide_notification_contents, options) do
|
||||||
query =
|
query =
|
||||||
from(u in Pleroma.User,
|
from(u in Pleroma.User,
|
||||||
update: [
|
update: [
|
||||||
set: [
|
set: [
|
||||||
notification_settings:
|
notification_settings:
|
||||||
fragment(
|
fragment(
|
||||||
"jsonb_set(notification_settings, '{privacy_option}', ?)",
|
"jsonb_set(notification_settings, '{hide_notification_contents}', ?)",
|
||||||
^privacy_option
|
^hide_notification_contents
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
76
lib/mix/tasks/pleroma/release_env.ex
Normal file
76
lib/mix/tasks/pleroma/release_env.ex
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Mix.Tasks.Pleroma.ReleaseEnv do
|
||||||
|
use Mix.Task
|
||||||
|
import Mix.Pleroma
|
||||||
|
|
||||||
|
@shortdoc "Generate Pleroma environment file."
|
||||||
|
@moduledoc File.read!("docs/administration/CLI_tasks/release_environments.md")
|
||||||
|
|
||||||
|
def run(["gen" | rest]) do
|
||||||
|
{options, [], []} =
|
||||||
|
OptionParser.parse(
|
||||||
|
rest,
|
||||||
|
strict: [
|
||||||
|
force: :boolean,
|
||||||
|
path: :string
|
||||||
|
],
|
||||||
|
aliases: [
|
||||||
|
p: :path,
|
||||||
|
f: :force
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
file_path =
|
||||||
|
get_option(
|
||||||
|
options,
|
||||||
|
:path,
|
||||||
|
"Environment file path",
|
||||||
|
"./config/pleroma.env"
|
||||||
|
)
|
||||||
|
|
||||||
|
env_path = Path.expand(file_path)
|
||||||
|
|
||||||
|
proceed? =
|
||||||
|
if File.exists?(env_path) do
|
||||||
|
get_option(
|
||||||
|
options,
|
||||||
|
:force,
|
||||||
|
"Environment file already exists. Do you want to overwrite the #{env_path} file? (y/n)",
|
||||||
|
"n"
|
||||||
|
) === "y"
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
if proceed? do
|
||||||
|
case do_generate(env_path) do
|
||||||
|
{:error, reason} ->
|
||||||
|
shell_error(
|
||||||
|
File.Error.message(%{action: "write to file", reason: reason, path: env_path})
|
||||||
|
)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
shell_info("\nThe file generated: #{env_path}.\n")
|
||||||
|
|
||||||
|
shell_info("""
|
||||||
|
WARNING: before start pleroma app please make sure to make the file read-only and non-modifiable.
|
||||||
|
Example:
|
||||||
|
chmod 0444 #{file_path}
|
||||||
|
chattr +i #{file_path}
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
shell_info("\nThe file is exist. #{env_path}.\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def do_generate(path) do
|
||||||
|
content = "RELEASE_COOKIE=#{Base.encode32(:crypto.strong_rand_bytes(32))}"
|
||||||
|
|
||||||
|
File.mkdir_p!(Path.dirname(path))
|
||||||
|
File.write(path, content)
|
||||||
|
end
|
||||||
|
end
|
|
@ -39,6 +39,7 @@ def start(_type, _args) do
|
||||||
# every time the application is restarted, so we disable module
|
# every time the application is restarted, so we disable module
|
||||||
# conflicts at runtime
|
# conflicts at runtime
|
||||||
Code.compiler_options(ignore_module_conflict: true)
|
Code.compiler_options(ignore_module_conflict: true)
|
||||||
|
Pleroma.Telemetry.Logger.attach()
|
||||||
Config.Holder.save_default()
|
Config.Holder.save_default()
|
||||||
Pleroma.HTML.compile_scrubbers()
|
Pleroma.HTML.compile_scrubbers()
|
||||||
Config.DeprecationWarnings.warn()
|
Config.DeprecationWarnings.warn()
|
||||||
|
@ -223,9 +224,7 @@ defp task_children(_) do
|
||||||
|
|
||||||
# start hackney and gun pools in tests
|
# start hackney and gun pools in tests
|
||||||
defp http_children(_, :test) do
|
defp http_children(_, :test) do
|
||||||
hackney_options = Config.get([:hackney_pools, :federation])
|
http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil)
|
||||||
hackney_pool = :hackney_pool.child_spec(:federation, hackney_options)
|
|
||||||
[hackney_pool, Pleroma.Pool.Supervisor]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp http_children(Tesla.Adapter.Hackney, _) do
|
defp http_children(Tesla.Adapter.Hackney, _) do
|
||||||
|
@ -244,7 +243,10 @@ defp http_children(Tesla.Adapter.Hackney, _) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp http_children(Tesla.Adapter.Gun, _), do: [Pleroma.Pool.Supervisor]
|
defp http_children(Tesla.Adapter.Gun, _) do
|
||||||
|
Pleroma.Gun.ConnectionPool.children() ++
|
||||||
|
[{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
|
||||||
|
end
|
||||||
|
|
||||||
defp http_children(_, _), do: []
|
defp http_children(_, _), do: []
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,9 @@ defmodule VerifyError, do: defexception([:message])
|
||||||
@spec verify!() :: :ok | VerifyError.t()
|
@spec verify!() :: :ok | VerifyError.t()
|
||||||
def verify! do
|
def verify! do
|
||||||
:ok
|
:ok
|
||||||
|
|> check_confirmation_accounts!
|
||||||
|> check_migrations_applied!()
|
|> check_migrations_applied!()
|
||||||
|
|> check_welcome_message_config!()
|
||||||
|> check_rum!()
|
|> check_rum!()
|
||||||
|> handle_result()
|
|> handle_result()
|
||||||
end
|
end
|
||||||
|
@ -24,6 +26,40 @@ def verify! do
|
||||||
defp handle_result(:ok), do: :ok
|
defp handle_result(:ok), do: :ok
|
||||||
defp handle_result({:error, message}), do: raise(VerifyError, message: message)
|
defp handle_result({:error, message}), do: raise(VerifyError, message: message)
|
||||||
|
|
||||||
|
defp check_welcome_message_config!(:ok) do
|
||||||
|
if Pleroma.Config.get([:welcome, :email, :enabled], false) and
|
||||||
|
not Pleroma.Emails.Mailer.enabled?() do
|
||||||
|
Logger.error("""
|
||||||
|
To send welcome email do you need to enable mail.
|
||||||
|
\nconfig :pleroma, Pleroma.Emails.Mailer, enabled: true
|
||||||
|
""")
|
||||||
|
|
||||||
|
{:error, "The mail disabled."}
|
||||||
|
else
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp check_welcome_message_config!(result), do: result
|
||||||
|
|
||||||
|
# Checks account confirmation email
|
||||||
|
#
|
||||||
|
def check_confirmation_accounts!(:ok) do
|
||||||
|
if Pleroma.Config.get([:instance, :account_activation_required]) &&
|
||||||
|
not Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
|
||||||
|
Logger.error(
|
||||||
|
"Account activation enabled, but no Mailer settings enabled.\nPlease set config :pleroma, :instance, account_activation_required: false\nOtherwise setup and enable Mailer."
|
||||||
|
)
|
||||||
|
|
||||||
|
{:error,
|
||||||
|
"Account activation enabled, but Mailer is disabled. Cannot send confirmation emails."}
|
||||||
|
else
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_confirmation_accounts!(result), do: result
|
||||||
|
|
||||||
# Checks for pending migrations.
|
# Checks for pending migrations.
|
||||||
#
|
#
|
||||||
def check_migrations_applied!(:ok) do
|
def check_migrations_applied!(:ok) do
|
||||||
|
|
|
@ -156,7 +156,6 @@ defp only_full_update?(%ConfigDB{group: group, key: key}) do
|
||||||
{:quack, :meta},
|
{:quack, :meta},
|
||||||
{:mime, :types},
|
{:mime, :types},
|
||||||
{:cors_plug, [:max_age, :methods, :expose, :headers]},
|
{:cors_plug, [:max_age, :methods, :expose, :headers]},
|
||||||
{:auto_linker, :opts},
|
|
||||||
{:swarm, :node_blacklist},
|
{:swarm, :node_blacklist},
|
||||||
{:logger, :backends}
|
{:logger, :backends}
|
||||||
]
|
]
|
||||||
|
|
|
@ -55,6 +55,24 @@ def warn do
|
||||||
mrf_user_allowlist()
|
mrf_user_allowlist()
|
||||||
check_old_mrf_config()
|
check_old_mrf_config()
|
||||||
check_media_proxy_whitelist_config()
|
check_media_proxy_whitelist_config()
|
||||||
|
check_welcome_message_config()
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_welcome_message_config do
|
||||||
|
instance_config = Pleroma.Config.get([:instance])
|
||||||
|
|
||||||
|
use_old_config =
|
||||||
|
Keyword.has_key?(instance_config, :welcome_user_nickname) or
|
||||||
|
Keyword.has_key?(instance_config, :welcome_message)
|
||||||
|
|
||||||
|
if use_old_config do
|
||||||
|
Logger.error("""
|
||||||
|
!!!DEPRECATION WARNING!!!
|
||||||
|
Your config is using the old namespace for Welcome messages configuration. You need to change to the new namespace:
|
||||||
|
\n* `config :pleroma, :instance, welcome_user_nickname` is now `config :pleroma, :welcome, :direct_message, :sender_nickname`
|
||||||
|
\n* `config :pleroma, :instance, welcome_message` is now `config :pleroma, :welcome, :direct_message, :message`
|
||||||
|
""")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_old_mrf_config do
|
def check_old_mrf_config do
|
||||||
|
|
17
lib/pleroma/config/helpers.ex
Normal file
17
lib/pleroma/config/helpers.ex
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Config.Helpers do
|
||||||
|
alias Pleroma.Config
|
||||||
|
|
||||||
|
def instance_name, do: Config.get([:instance, :name])
|
||||||
|
|
||||||
|
defp instance_notify_email do
|
||||||
|
Config.get([:instance, :notify_email]) || Config.get([:instance, :email])
|
||||||
|
end
|
||||||
|
|
||||||
|
def sender do
|
||||||
|
{instance_name(), instance_notify_email()}
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Emails.AdminEmail do
|
||||||
import Swoosh.Email
|
import Swoosh.Email
|
||||||
|
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.HTML
|
||||||
alias Pleroma.Web.Router.Helpers
|
alias Pleroma.Web.Router.Helpers
|
||||||
|
|
||||||
defp instance_config, do: Config.get(:instance)
|
defp instance_config, do: Config.get(:instance)
|
||||||
|
@ -82,4 +83,18 @@ def report(to, reporter, account, statuses, comment) do
|
||||||
|> subject("#{instance_name()} Report")
|
|> subject("#{instance_name()} Report")
|
||||||
|> html_body(html_body)
|
|> html_body(html_body)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def new_unapproved_registration(to, account) do
|
||||||
|
html_body = """
|
||||||
|
<p>New account for review: <a href="#{user_url(account)}">@#{account.nickname}</a></p>
|
||||||
|
<blockquote>#{HTML.strip_tags(account.registration_reason)}</blockquote>
|
||||||
|
<a href="#{Pleroma.Web.base_url()}/pleroma/admin">Visit AdminFE</a>
|
||||||
|
"""
|
||||||
|
|
||||||
|
new()
|
||||||
|
|> to({to.name, to.email})
|
||||||
|
|> from({instance_name(), instance_notify_email()})
|
||||||
|
|> subject("New account up for review on #{instance_name()} (@#{account.nickname})")
|
||||||
|
|> html_body(html_body)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,17 +12,22 @@ defmodule Pleroma.Emails.UserEmail do
|
||||||
alias Pleroma.Web.Endpoint
|
alias Pleroma.Web.Endpoint
|
||||||
alias Pleroma.Web.Router
|
alias Pleroma.Web.Router
|
||||||
|
|
||||||
defp instance_name, do: Config.get([:instance, :name])
|
import Pleroma.Config.Helpers, only: [instance_name: 0, sender: 0]
|
||||||
|
|
||||||
defp sender do
|
|
||||||
email = Config.get([:instance, :notify_email]) || Config.get([:instance, :email])
|
|
||||||
{instance_name(), email}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp recipient(email, nil), do: email
|
defp recipient(email, nil), do: email
|
||||||
defp recipient(email, name), do: {name, email}
|
defp recipient(email, name), do: {name, email}
|
||||||
defp recipient(%User{} = user), do: recipient(user.email, user.name)
|
defp recipient(%User{} = user), do: recipient(user.email, user.name)
|
||||||
|
|
||||||
|
@spec welcome(User.t(), map()) :: Swoosh.Email.t()
|
||||||
|
def welcome(user, opts \\ %{}) do
|
||||||
|
new()
|
||||||
|
|> to(recipient(user))
|
||||||
|
|> from(Map.get(opts, :sender, sender()))
|
||||||
|
|> subject(Map.get(opts, :subject, "Welcome to #{instance_name()}!"))
|
||||||
|
|> html_body(Map.get(opts, :html, "Welcome to #{instance_name()}!"))
|
||||||
|
|> text_body(Map.get(opts, :text, "Welcome to #{instance_name()}!"))
|
||||||
|
end
|
||||||
|
|
||||||
def password_reset_email(user, token) when is_binary(token) do
|
def password_reset_email(user, token) when is_binary(token) do
|
||||||
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
|
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,15 @@ defmodule Pleroma.Formatter do
|
||||||
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
|
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
|
||||||
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
|
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
|
||||||
|
|
||||||
@auto_linker_config hashtag: true,
|
defp linkify_opts do
|
||||||
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
|
Pleroma.Config.get(Pleroma.Formatter) ++
|
||||||
mention: true,
|
[
|
||||||
mention_handler: &Pleroma.Formatter.mention_handler/4,
|
hashtag: true,
|
||||||
scheme: true
|
hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
|
||||||
|
mention: true,
|
||||||
|
mention_handler: &Pleroma.Formatter.mention_handler/4
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
|
def escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
|
||||||
case User.get_cached_by_nickname(nickname) do
|
case User.get_cached_by_nickname(nickname) do
|
||||||
|
@ -80,19 +84,19 @@ def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
|
||||||
@spec linkify(String.t(), keyword()) ::
|
@spec linkify(String.t(), keyword()) ::
|
||||||
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
|
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
|
||||||
def linkify(text, options \\ []) do
|
def linkify(text, options \\ []) do
|
||||||
options = options ++ @auto_linker_config
|
options = linkify_opts() ++ options
|
||||||
|
|
||||||
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
|
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
|
||||||
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
|
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
|
||||||
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
|
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
|
||||||
|
|
||||||
{text_mentions, %{mentions: mentions}} = AutoLinker.link_map(mentions, acc, options)
|
{text_mentions, %{mentions: mentions}} = Linkify.link_map(mentions, acc, options)
|
||||||
{text_rest, %{tags: tags}} = AutoLinker.link_map(rest, acc, options)
|
{text_rest, %{tags: tags}} = Linkify.link_map(rest, acc, options)
|
||||||
|
|
||||||
{text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
|
{text_mentions <> text_rest, MapSet.to_list(mentions), MapSet.to_list(tags)}
|
||||||
else
|
else
|
||||||
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
|
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
|
||||||
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
|
{text, %{mentions: mentions, tags: tags}} = Linkify.link_map(text, acc, options)
|
||||||
|
|
||||||
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
|
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
|
||||||
end
|
end
|
||||||
|
@ -111,9 +115,9 @@ def mentions_escape(text, options \\ []) do
|
||||||
|
|
||||||
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
|
if options[:safe_mention] && Regex.named_captures(@safe_mention_regex, text) do
|
||||||
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
|
%{"mentions" => mentions, "rest" => rest} = Regex.named_captures(@safe_mention_regex, text)
|
||||||
AutoLinker.link(mentions, options) <> AutoLinker.link(rest, options)
|
Linkify.link(mentions, options) <> Linkify.link(rest, options)
|
||||||
else
|
else
|
||||||
AutoLinker.link(text, options)
|
Linkify.link(text, options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -96,16 +96,18 @@ def response("") do
|
||||||
|
|
||||||
def response("/main/public") do
|
def response("/main/public") do
|
||||||
posts =
|
posts =
|
||||||
ActivityPub.fetch_public_activities(%{"type" => ["Create"], "local_only" => true})
|
%{type: ["Create"], local_only: true}
|
||||||
|> render_activities
|
|> ActivityPub.fetch_public_activities()
|
||||||
|
|> render_activities()
|
||||||
|
|
||||||
info("Welcome to the Public Timeline!") <> posts <> ".\r\n"
|
info("Welcome to the Public Timeline!") <> posts <> ".\r\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
def response("/main/all") do
|
def response("/main/all") do
|
||||||
posts =
|
posts =
|
||||||
ActivityPub.fetch_public_activities(%{"type" => ["Create"]})
|
%{type: ["Create"]}
|
||||||
|> render_activities
|
|> ActivityPub.fetch_public_activities()
|
||||||
|
|> render_activities()
|
||||||
|
|
||||||
info("Welcome to the Federated Timeline!") <> posts <> ".\r\n"
|
info("Welcome to the Federated Timeline!") <> posts <> ".\r\n"
|
||||||
end
|
end
|
||||||
|
@ -130,13 +132,14 @@ def response("/notices/" <> id) do
|
||||||
def response("/users/" <> nickname) do
|
def response("/users/" <> nickname) do
|
||||||
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
params = %{
|
params = %{
|
||||||
"type" => ["Create"],
|
type: ["Create"],
|
||||||
"actor_id" => user.ap_id
|
actor_id: user.ap_id
|
||||||
}
|
}
|
||||||
|
|
||||||
activities =
|
activities =
|
||||||
ActivityPub.fetch_public_activities(params)
|
params
|
||||||
|> render_activities
|
|> ActivityPub.fetch_public_activities()
|
||||||
|
|> render_activities()
|
||||||
|
|
||||||
info("Posts by #{user.nickname}") <> activities <> ".\r\n"
|
info("Posts by #{user.nickname}") <> activities <> ".\r\n"
|
||||||
else
|
else
|
||||||
|
|
|
@ -19,7 +19,8 @@ defmodule Pleroma.Gun.API do
|
||||||
:tls_opts,
|
:tls_opts,
|
||||||
:tcp_opts,
|
:tcp_opts,
|
||||||
:socks_opts,
|
:socks_opts,
|
||||||
:ws_opts
|
:ws_opts,
|
||||||
|
:supervise
|
||||||
]
|
]
|
||||||
|
|
||||||
@impl Gun
|
@impl Gun
|
||||||
|
|
|
@ -3,85 +3,33 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Gun.Conn do
|
defmodule Pleroma.Gun.Conn do
|
||||||
@moduledoc """
|
|
||||||
Struct for gun connection data
|
|
||||||
"""
|
|
||||||
alias Pleroma.Gun
|
alias Pleroma.Gun
|
||||||
alias Pleroma.Pool.Connections
|
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@type gun_state :: :up | :down
|
def open(%URI{} = uri, opts) do
|
||||||
@type conn_state :: :active | :idle
|
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
|
||||||
conn: pid(),
|
|
||||||
gun_state: gun_state(),
|
|
||||||
conn_state: conn_state(),
|
|
||||||
used_by: [pid()],
|
|
||||||
last_reference: pos_integer(),
|
|
||||||
crf: float(),
|
|
||||||
retries: pos_integer()
|
|
||||||
}
|
|
||||||
|
|
||||||
defstruct conn: nil,
|
|
||||||
gun_state: :open,
|
|
||||||
conn_state: :init,
|
|
||||||
used_by: [],
|
|
||||||
last_reference: 0,
|
|
||||||
crf: 1,
|
|
||||||
retries: 0
|
|
||||||
|
|
||||||
@spec open(String.t() | URI.t(), atom(), keyword()) :: :ok | nil
|
|
||||||
def open(url, name, opts \\ [])
|
|
||||||
def open(url, name, opts) when is_binary(url), do: open(URI.parse(url), name, opts)
|
|
||||||
|
|
||||||
def open(%URI{} = uri, name, opts) do
|
|
||||||
pool_opts = Pleroma.Config.get([:connections_pool], [])
|
pool_opts = Pleroma.Config.get([:connections_pool], [])
|
||||||
|
|
||||||
opts =
|
opts =
|
||||||
opts
|
opts
|
||||||
|> Enum.into(%{})
|
|> Enum.into(%{})
|
||||||
|> Map.put_new(:retry, pool_opts[:retry] || 1)
|
|
||||||
|> Map.put_new(:retry_timeout, pool_opts[:retry_timeout] || 1000)
|
|
||||||
|> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000)
|
|> Map.put_new(:await_up_timeout, pool_opts[:await_up_timeout] || 5_000)
|
||||||
|
|> Map.put_new(:supervise, false)
|
||||||
|> maybe_add_tls_opts(uri)
|
|> maybe_add_tls_opts(uri)
|
||||||
|
|
||||||
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
|
do_open(uri, opts)
|
||||||
|
|
||||||
max_connections = pool_opts[:max_connections] || 250
|
|
||||||
|
|
||||||
conn_pid =
|
|
||||||
if Connections.count(name) < max_connections do
|
|
||||||
do_open(uri, opts)
|
|
||||||
else
|
|
||||||
close_least_used_and_do_open(name, uri, opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
if is_pid(conn_pid) do
|
|
||||||
conn = %Pleroma.Gun.Conn{
|
|
||||||
conn: conn_pid,
|
|
||||||
gun_state: :up,
|
|
||||||
conn_state: :active,
|
|
||||||
last_reference: :os.system_time(:second)
|
|
||||||
}
|
|
||||||
|
|
||||||
:ok = Gun.set_owner(conn_pid, Process.whereis(name))
|
|
||||||
Connections.add_conn(name, key, conn)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_add_tls_opts(opts, %URI{scheme: "http"}), do: opts
|
defp maybe_add_tls_opts(opts, %URI{scheme: "http"}), do: opts
|
||||||
|
|
||||||
defp maybe_add_tls_opts(opts, %URI{scheme: "https", host: host}) do
|
defp maybe_add_tls_opts(opts, %URI{scheme: "https"}) do
|
||||||
tls_opts = [
|
tls_opts = [
|
||||||
verify: :verify_peer,
|
verify: :verify_peer,
|
||||||
cacertfile: CAStore.file_path(),
|
cacertfile: CAStore.file_path(),
|
||||||
depth: 20,
|
depth: 20,
|
||||||
reuse_sessions: false,
|
reuse_sessions: false,
|
||||||
verify_fun:
|
log_level: :warning,
|
||||||
{&:ssl_verify_hostname.verify_fun/3,
|
customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]
|
||||||
[check_hostname: Pleroma.HTTP.Connection.format_host(host)]}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
tls_opts =
|
tls_opts =
|
||||||
|
@ -105,7 +53,7 @@ defp do_open(uri, %{proxy: {proxy_host, proxy_port}} = opts) do
|
||||||
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]),
|
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]),
|
||||||
stream <- Gun.connect(conn, connect_opts),
|
stream <- Gun.connect(conn, connect_opts),
|
||||||
{:response, :fin, 200, _} <- Gun.await(conn, stream) do
|
{:response, :fin, 200, _} <- Gun.await(conn, stream) do
|
||||||
conn
|
{:ok, conn}
|
||||||
else
|
else
|
||||||
error ->
|
error ->
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
|
@ -141,7 +89,7 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do
|
||||||
|
|
||||||
with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts),
|
with {:ok, conn} <- Gun.open(proxy_host, proxy_port, opts),
|
||||||
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
|
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
|
||||||
conn
|
{:ok, conn}
|
||||||
else
|
else
|
||||||
error ->
|
error ->
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
|
@ -155,11 +103,11 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_open(%URI{host: host, port: port} = uri, opts) do
|
defp do_open(%URI{host: host, port: port} = uri, opts) do
|
||||||
host = Pleroma.HTTP.Connection.parse_host(host)
|
host = Pleroma.HTTP.AdapterHelper.parse_host(host)
|
||||||
|
|
||||||
with {:ok, conn} <- Gun.open(host, port, opts),
|
with {:ok, conn} <- Gun.open(host, port, opts),
|
||||||
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
|
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
|
||||||
conn
|
{:ok, conn}
|
||||||
else
|
else
|
||||||
error ->
|
error ->
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
|
@ -171,7 +119,7 @@ defp do_open(%URI{host: host, port: port} = uri, opts) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp destination_opts(%URI{host: host, port: port}) do
|
defp destination_opts(%URI{host: host, port: port}) do
|
||||||
host = Pleroma.HTTP.Connection.parse_host(host)
|
host = Pleroma.HTTP.AdapterHelper.parse_host(host)
|
||||||
%{host: host, port: port}
|
%{host: host, port: port}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -181,17 +129,6 @@ defp add_http2_opts(opts, "https", tls_opts) do
|
||||||
|
|
||||||
defp add_http2_opts(opts, _, _), do: opts
|
defp add_http2_opts(opts, _, _), do: opts
|
||||||
|
|
||||||
defp close_least_used_and_do_open(name, uri, opts) do
|
|
||||||
with [{key, conn} | _conns] <- Connections.get_unused_conns(name),
|
|
||||||
:ok <- Gun.close(conn.conn) do
|
|
||||||
Connections.remove_conn(name, key)
|
|
||||||
|
|
||||||
do_open(uri, opts)
|
|
||||||
else
|
|
||||||
[] -> {:error, :pool_overflowed}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def compose_uri_log(%URI{scheme: scheme, host: host, path: path}) do
|
def compose_uri_log(%URI{scheme: scheme, host: host, path: path}) do
|
||||||
"#{scheme}://#{host}#{path}"
|
"#{scheme}://#{host}#{path}"
|
||||||
end
|
end
|
||||||
|
|
79
lib/pleroma/gun/connection_pool.ex
Normal file
79
lib/pleroma/gun/connection_pool.ex
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
defmodule Pleroma.Gun.ConnectionPool do
|
||||||
|
@registry __MODULE__
|
||||||
|
|
||||||
|
alias Pleroma.Gun.ConnectionPool.WorkerSupervisor
|
||||||
|
|
||||||
|
def children do
|
||||||
|
[
|
||||||
|
{Registry, keys: :unique, name: @registry},
|
||||||
|
Pleroma.Gun.ConnectionPool.WorkerSupervisor
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_conn(uri, opts) do
|
||||||
|
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
|
||||||
|
|
||||||
|
case Registry.lookup(@registry, key) do
|
||||||
|
# The key has already been registered, but connection is not up yet
|
||||||
|
[{worker_pid, nil}] ->
|
||||||
|
get_gun_pid_from_worker(worker_pid, true)
|
||||||
|
|
||||||
|
[{worker_pid, {gun_pid, _used_by, _crf, _last_reference}}] ->
|
||||||
|
GenServer.call(worker_pid, :add_client)
|
||||||
|
{:ok, gun_pid}
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
# :gun.set_owner fails in :connected state for whatevever reason,
|
||||||
|
# so we open the connection in the process directly and send it's pid back
|
||||||
|
# We trust gun to handle timeouts by itself
|
||||||
|
case WorkerSupervisor.start_worker([key, uri, opts, self()]) do
|
||||||
|
{:ok, worker_pid} ->
|
||||||
|
get_gun_pid_from_worker(worker_pid, false)
|
||||||
|
|
||||||
|
{:error, {:already_started, worker_pid}} ->
|
||||||
|
get_gun_pid_from_worker(worker_pid, true)
|
||||||
|
|
||||||
|
err ->
|
||||||
|
err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_gun_pid_from_worker(worker_pid, register) do
|
||||||
|
# GenServer.call will block the process for timeout length if
|
||||||
|
# the server crashes on startup (which will happen if gun fails to connect)
|
||||||
|
# so instead we use cast + monitor
|
||||||
|
|
||||||
|
ref = Process.monitor(worker_pid)
|
||||||
|
if register, do: GenServer.cast(worker_pid, {:add_client, self()})
|
||||||
|
|
||||||
|
receive do
|
||||||
|
{:conn_pid, pid} ->
|
||||||
|
Process.demonitor(ref)
|
||||||
|
{:ok, pid}
|
||||||
|
|
||||||
|
{:DOWN, ^ref, :process, ^worker_pid, reason} ->
|
||||||
|
case reason do
|
||||||
|
{:shutdown, error} -> error
|
||||||
|
_ -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_conn(conn_pid) do
|
||||||
|
# :ets.fun2ms(fn {_, {worker_pid, {gun_pid, _, _, _}}} when gun_pid == conn_pid ->
|
||||||
|
# worker_pid end)
|
||||||
|
query_result =
|
||||||
|
Registry.select(@registry, [
|
||||||
|
{{:_, :"$1", {:"$2", :_, :_, :_}}, [{:==, :"$2", conn_pid}], [:"$1"]}
|
||||||
|
])
|
||||||
|
|
||||||
|
case query_result do
|
||||||
|
[worker_pid] ->
|
||||||
|
GenServer.call(worker_pid, :remove_client)
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
85
lib/pleroma/gun/connection_pool/reclaimer.ex
Normal file
85
lib/pleroma/gun/connection_pool/reclaimer.ex
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
defmodule Pleroma.Gun.ConnectionPool.Reclaimer do
|
||||||
|
use GenServer, restart: :temporary
|
||||||
|
|
||||||
|
@registry Pleroma.Gun.ConnectionPool
|
||||||
|
|
||||||
|
def start_monitor do
|
||||||
|
pid =
|
||||||
|
case :gen_server.start(__MODULE__, [], name: {:via, Registry, {@registry, "reclaimer"}}) do
|
||||||
|
{:ok, pid} ->
|
||||||
|
pid
|
||||||
|
|
||||||
|
{:error, {:already_registered, pid}} ->
|
||||||
|
pid
|
||||||
|
end
|
||||||
|
|
||||||
|
{pid, Process.monitor(pid)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_) do
|
||||||
|
{:ok, nil, {:continue, :reclaim}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_continue(:reclaim, _) do
|
||||||
|
max_connections = Pleroma.Config.get([:connections_pool, :max_connections])
|
||||||
|
|
||||||
|
reclaim_max =
|
||||||
|
[:connections_pool, :reclaim_multiplier]
|
||||||
|
|> Pleroma.Config.get()
|
||||||
|
|> Kernel.*(max_connections)
|
||||||
|
|> round
|
||||||
|
|> max(1)
|
||||||
|
|
||||||
|
:telemetry.execute([:pleroma, :connection_pool, :reclaim, :start], %{}, %{
|
||||||
|
max_connections: max_connections,
|
||||||
|
reclaim_max: reclaim_max
|
||||||
|
})
|
||||||
|
|
||||||
|
# :ets.fun2ms(
|
||||||
|
# fn {_, {worker_pid, {_, used_by, crf, last_reference}}} when used_by == [] ->
|
||||||
|
# {worker_pid, crf, last_reference} end)
|
||||||
|
unused_conns =
|
||||||
|
Registry.select(
|
||||||
|
@registry,
|
||||||
|
[
|
||||||
|
{{:_, :"$1", {:_, :"$2", :"$3", :"$4"}}, [{:==, :"$2", []}], [{{:"$1", :"$3", :"$4"}}]}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
case unused_conns do
|
||||||
|
[] ->
|
||||||
|
:telemetry.execute(
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :stop],
|
||||||
|
%{reclaimed_count: 0},
|
||||||
|
%{
|
||||||
|
max_connections: max_connections
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
{:stop, :no_unused_conns, nil}
|
||||||
|
|
||||||
|
unused_conns ->
|
||||||
|
reclaimed =
|
||||||
|
unused_conns
|
||||||
|
|> Enum.sort(fn {_pid1, crf1, last_reference1}, {_pid2, crf2, last_reference2} ->
|
||||||
|
crf1 <= crf2 and last_reference1 <= last_reference2
|
||||||
|
end)
|
||||||
|
|> Enum.take(reclaim_max)
|
||||||
|
|
||||||
|
reclaimed
|
||||||
|
|> Enum.each(fn {pid, _, _} ->
|
||||||
|
DynamicSupervisor.terminate_child(Pleroma.Gun.ConnectionPool.WorkerSupervisor, pid)
|
||||||
|
end)
|
||||||
|
|
||||||
|
:telemetry.execute(
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :stop],
|
||||||
|
%{reclaimed_count: Enum.count(reclaimed)},
|
||||||
|
%{max_connections: max_connections}
|
||||||
|
)
|
||||||
|
|
||||||
|
{:stop, :normal, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
133
lib/pleroma/gun/connection_pool/worker.ex
Normal file
133
lib/pleroma/gun/connection_pool/worker.ex
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
defmodule Pleroma.Gun.ConnectionPool.Worker do
|
||||||
|
alias Pleroma.Gun
|
||||||
|
use GenServer, restart: :temporary
|
||||||
|
|
||||||
|
@registry Pleroma.Gun.ConnectionPool
|
||||||
|
|
||||||
|
def start_link([key | _] = opts) do
|
||||||
|
GenServer.start_link(__MODULE__, opts, name: {:via, Registry, {@registry, key}})
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init([_key, _uri, _opts, _client_pid] = opts) do
|
||||||
|
{:ok, nil, {:continue, {:connect, opts}}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do
|
||||||
|
with {:ok, conn_pid} <- Gun.Conn.open(uri, opts),
|
||||||
|
Process.link(conn_pid) do
|
||||||
|
time = :erlang.monotonic_time(:millisecond)
|
||||||
|
|
||||||
|
{_, _} =
|
||||||
|
Registry.update_value(@registry, key, fn _ ->
|
||||||
|
{conn_pid, [client_pid], 1, time}
|
||||||
|
end)
|
||||||
|
|
||||||
|
send(client_pid, {:conn_pid, conn_pid})
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
%{key: key, timer: nil, client_monitors: %{client_pid => Process.monitor(client_pid)}},
|
||||||
|
:hibernate}
|
||||||
|
else
|
||||||
|
err ->
|
||||||
|
{:stop, {:shutdown, err}, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_cast({:add_client, client_pid}, state) do
|
||||||
|
case handle_call(:add_client, {client_pid, nil}, state) do
|
||||||
|
{:reply, conn_pid, state, :hibernate} ->
|
||||||
|
send(client_pid, {:conn_pid, conn_pid})
|
||||||
|
{:noreply, state, :hibernate}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_cast({:remove_client, client_pid}, state) do
|
||||||
|
case handle_call(:remove_client, {client_pid, nil}, state) do
|
||||||
|
{:reply, _, state, :hibernate} ->
|
||||||
|
{:noreply, state, :hibernate}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:add_client, {client_pid, _}, %{key: key} = state) do
|
||||||
|
time = :erlang.monotonic_time(:millisecond)
|
||||||
|
|
||||||
|
{{conn_pid, _, _, _}, _} =
|
||||||
|
Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} ->
|
||||||
|
{conn_pid, [client_pid | used_by], crf(time - last_reference, crf), time}
|
||||||
|
end)
|
||||||
|
|
||||||
|
state =
|
||||||
|
if state.timer != nil do
|
||||||
|
Process.cancel_timer(state[:timer])
|
||||||
|
%{state | timer: nil}
|
||||||
|
else
|
||||||
|
state
|
||||||
|
end
|
||||||
|
|
||||||
|
ref = Process.monitor(client_pid)
|
||||||
|
|
||||||
|
state = put_in(state.client_monitors[client_pid], ref)
|
||||||
|
{:reply, conn_pid, state, :hibernate}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:remove_client, {client_pid, _}, %{key: key} = state) do
|
||||||
|
{{_conn_pid, used_by, _crf, _last_reference}, _} =
|
||||||
|
Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} ->
|
||||||
|
{conn_pid, List.delete(used_by, client_pid), crf, last_reference}
|
||||||
|
end)
|
||||||
|
|
||||||
|
{ref, state} = pop_in(state.client_monitors[client_pid])
|
||||||
|
Process.demonitor(ref)
|
||||||
|
|
||||||
|
timer =
|
||||||
|
if used_by == [] do
|
||||||
|
max_idle = Pleroma.Config.get([:connections_pool, :max_idle_time], 30_000)
|
||||||
|
Process.send_after(self(), :idle_close, max_idle)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
{:reply, :ok, %{state | timer: timer}, :hibernate}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:idle_close, state) do
|
||||||
|
# Gun monitors the owner process, and will close the connection automatically
|
||||||
|
# when it's terminated
|
||||||
|
{:stop, :normal, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Gracefully shutdown if the connection got closed without any streams left
|
||||||
|
@impl true
|
||||||
|
def handle_info({:gun_down, _pid, _protocol, _reason, []}, state) do
|
||||||
|
{:stop, :normal, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Otherwise, shutdown with an error
|
||||||
|
@impl true
|
||||||
|
def handle_info({:gun_down, _pid, _protocol, _reason, _killed_streams} = down_message, state) do
|
||||||
|
{:stop, {:error, down_message}, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
|
||||||
|
:telemetry.execute(
|
||||||
|
[:pleroma, :connection_pool, :client_death],
|
||||||
|
%{client_pid: pid, reason: reason},
|
||||||
|
%{key: state.key}
|
||||||
|
)
|
||||||
|
|
||||||
|
handle_cast({:remove_client, pid}, state)
|
||||||
|
end
|
||||||
|
|
||||||
|
# LRFU policy: https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.55.1478
|
||||||
|
defp crf(time_delta, prev_crf) do
|
||||||
|
1 + :math.pow(0.5, 0.0001 * time_delta) * prev_crf
|
||||||
|
end
|
||||||
|
end
|
45
lib/pleroma/gun/connection_pool/worker_supervisor.ex
Normal file
45
lib/pleroma/gun/connection_pool/worker_supervisor.ex
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do
|
||||||
|
@moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit"
|
||||||
|
|
||||||
|
use DynamicSupervisor
|
||||||
|
|
||||||
|
def start_link(opts) do
|
||||||
|
DynamicSupervisor.start_link(__MODULE__, opts, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(_opts) do
|
||||||
|
DynamicSupervisor.init(
|
||||||
|
strategy: :one_for_one,
|
||||||
|
max_children: Pleroma.Config.get([:connections_pool, :max_connections])
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_worker(opts, retry \\ false) do
|
||||||
|
case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do
|
||||||
|
{:error, :max_children} ->
|
||||||
|
if retry or free_pool() == :error do
|
||||||
|
:telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
|
||||||
|
{:error, :pool_full}
|
||||||
|
else
|
||||||
|
start_worker(opts, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
res ->
|
||||||
|
res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp free_pool do
|
||||||
|
wait_for_reclaimer_finish(Pleroma.Gun.ConnectionPool.Reclaimer.start_monitor())
|
||||||
|
end
|
||||||
|
|
||||||
|
defp wait_for_reclaimer_finish({pid, mon}) do
|
||||||
|
receive do
|
||||||
|
{:DOWN, ^mon, :process, ^pid, :no_unused_conns} ->
|
||||||
|
:error
|
||||||
|
|
||||||
|
{:DOWN, ^mon, :process, ^pid, :normal} ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,32 +3,30 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.HTTP.AdapterHelper do
|
defmodule Pleroma.HTTP.AdapterHelper do
|
||||||
alias Pleroma.HTTP.Connection
|
@moduledoc """
|
||||||
|
Configure Tesla.Client with default and customized adapter options.
|
||||||
|
"""
|
||||||
|
@defaults [pool: :federation]
|
||||||
|
|
||||||
|
@type proxy_type() :: :socks4 | :socks5
|
||||||
|
@type host() :: charlist() | :inet.ip_address()
|
||||||
|
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.HTTP.AdapterHelper
|
||||||
|
require Logger
|
||||||
|
|
||||||
@type proxy ::
|
@type proxy ::
|
||||||
{Connection.host(), pos_integer()}
|
{Connection.host(), pos_integer()}
|
||||||
| {Connection.proxy_type(), Connection.host(), pos_integer()}
|
| {Connection.proxy_type(), Connection.host(), pos_integer()}
|
||||||
|
|
||||||
@callback options(keyword(), URI.t()) :: keyword()
|
@callback options(keyword(), URI.t()) :: keyword()
|
||||||
@callback after_request(keyword()) :: :ok
|
@callback get_conn(URI.t(), keyword()) :: {:ok, term()} | {:error, term()}
|
||||||
|
|
||||||
@spec options(keyword(), URI.t()) :: keyword()
|
|
||||||
def options(opts, _uri) do
|
|
||||||
proxy = Pleroma.Config.get([:http, :proxy_url], nil)
|
|
||||||
maybe_add_proxy(opts, format_proxy(proxy))
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec maybe_get_conn(URI.t(), keyword()) :: keyword()
|
|
||||||
def maybe_get_conn(_uri, opts), do: opts
|
|
||||||
|
|
||||||
@spec after_request(keyword()) :: :ok
|
|
||||||
def after_request(_opts), do: :ok
|
|
||||||
|
|
||||||
@spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil
|
@spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil
|
||||||
def format_proxy(nil), do: nil
|
def format_proxy(nil), do: nil
|
||||||
|
|
||||||
def format_proxy(proxy_url) do
|
def format_proxy(proxy_url) do
|
||||||
case Connection.parse_proxy(proxy_url) do
|
case parse_proxy(proxy_url) do
|
||||||
{:ok, host, port} -> {host, port}
|
{:ok, host, port} -> {host, port}
|
||||||
{:ok, type, host, port} -> {type, host, port}
|
{:ok, type, host, port} -> {type, host, port}
|
||||||
_ -> nil
|
_ -> nil
|
||||||
|
@ -38,4 +36,105 @@ def format_proxy(proxy_url) do
|
||||||
@spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword()
|
@spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword()
|
||||||
def maybe_add_proxy(opts, nil), do: opts
|
def maybe_add_proxy(opts, nil), do: opts
|
||||||
def maybe_add_proxy(opts, proxy), do: Keyword.put_new(opts, :proxy, proxy)
|
def maybe_add_proxy(opts, proxy), do: Keyword.put_new(opts, :proxy, proxy)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Merge default connection & adapter options with received ones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@spec options(URI.t(), keyword()) :: keyword()
|
||||||
|
def options(%URI{} = uri, opts \\ []) do
|
||||||
|
@defaults
|
||||||
|
|> put_timeout()
|
||||||
|
|> Keyword.merge(opts)
|
||||||
|
|> adapter_helper().options(uri)
|
||||||
|
end
|
||||||
|
|
||||||
|
# For Hackney, this is the time a connection can stay idle in the pool.
|
||||||
|
# For Gun, this is the timeout to receive a message from Gun.
|
||||||
|
defp put_timeout(opts) do
|
||||||
|
{config_key, default} =
|
||||||
|
if adapter() == Tesla.Adapter.Gun do
|
||||||
|
{:pools, Config.get([:pools, :default, :timeout], 5_000)}
|
||||||
|
else
|
||||||
|
{:hackney_pools, 10_000}
|
||||||
|
end
|
||||||
|
|
||||||
|
timeout = Config.get([config_key, opts[:pool], :timeout], default)
|
||||||
|
|
||||||
|
Keyword.merge(opts, timeout: timeout)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_conn(uri, opts), do: adapter_helper().get_conn(uri, opts)
|
||||||
|
defp adapter, do: Application.get_env(:tesla, :adapter)
|
||||||
|
|
||||||
|
defp adapter_helper do
|
||||||
|
case adapter() do
|
||||||
|
Tesla.Adapter.Gun -> AdapterHelper.Gun
|
||||||
|
Tesla.Adapter.Hackney -> AdapterHelper.Hackney
|
||||||
|
_ -> AdapterHelper.Default
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec parse_proxy(String.t() | tuple() | nil) ::
|
||||||
|
{:ok, host(), pos_integer()}
|
||||||
|
| {:ok, proxy_type(), host(), pos_integer()}
|
||||||
|
| {:error, atom()}
|
||||||
|
| nil
|
||||||
|
|
||||||
|
def parse_proxy(nil), do: nil
|
||||||
|
|
||||||
|
def parse_proxy(proxy) when is_binary(proxy) do
|
||||||
|
with [host, port] <- String.split(proxy, ":"),
|
||||||
|
{port, ""} <- Integer.parse(port) do
|
||||||
|
{:ok, parse_host(host), port}
|
||||||
|
else
|
||||||
|
{_, _} ->
|
||||||
|
Logger.warn("Parsing port failed #{inspect(proxy)}")
|
||||||
|
{:error, :invalid_proxy_port}
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
Logger.warn("Parsing port failed #{inspect(proxy)}")
|
||||||
|
{:error, :invalid_proxy_port}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
|
||||||
|
{:error, :invalid_proxy}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_proxy(proxy) when is_tuple(proxy) do
|
||||||
|
with {type, host, port} <- proxy do
|
||||||
|
{:ok, type, parse_host(host), port}
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
|
||||||
|
{:error, :invalid_proxy}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec parse_host(String.t() | atom() | charlist()) :: charlist() | :inet.ip_address()
|
||||||
|
def parse_host(host) when is_list(host), do: host
|
||||||
|
def parse_host(host) when is_atom(host), do: to_charlist(host)
|
||||||
|
|
||||||
|
def parse_host(host) when is_binary(host) do
|
||||||
|
host = to_charlist(host)
|
||||||
|
|
||||||
|
case :inet.parse_address(host) do
|
||||||
|
{:error, :einval} -> host
|
||||||
|
{:ok, ip} -> ip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec format_host(String.t()) :: charlist()
|
||||||
|
def format_host(host) do
|
||||||
|
host_charlist = to_charlist(host)
|
||||||
|
|
||||||
|
case :inet.parse_address(host_charlist) do
|
||||||
|
{:error, :einval} ->
|
||||||
|
:idna.encode(host_charlist)
|
||||||
|
|
||||||
|
{:ok, _ip} ->
|
||||||
|
host_charlist
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
14
lib/pleroma/http/adapter_helper/default.ex
Normal file
14
lib/pleroma/http/adapter_helper/default.ex
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
defmodule Pleroma.HTTP.AdapterHelper.Default do
|
||||||
|
alias Pleroma.HTTP.AdapterHelper
|
||||||
|
|
||||||
|
@behaviour Pleroma.HTTP.AdapterHelper
|
||||||
|
|
||||||
|
@spec options(keyword(), URI.t()) :: keyword()
|
||||||
|
def options(opts, _uri) do
|
||||||
|
proxy = Pleroma.Config.get([:http, :proxy_url], nil)
|
||||||
|
AdapterHelper.maybe_add_proxy(opts, AdapterHelper.format_proxy(proxy))
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_conn(URI.t(), keyword()) :: {:ok, keyword()}
|
||||||
|
def get_conn(_uri, opts), do: {:ok, opts}
|
||||||
|
end
|
|
@ -5,8 +5,8 @@
|
||||||
defmodule Pleroma.HTTP.AdapterHelper.Gun do
|
defmodule Pleroma.HTTP.AdapterHelper.Gun do
|
||||||
@behaviour Pleroma.HTTP.AdapterHelper
|
@behaviour Pleroma.HTTP.AdapterHelper
|
||||||
|
|
||||||
|
alias Pleroma.Gun.ConnectionPool
|
||||||
alias Pleroma.HTTP.AdapterHelper
|
alias Pleroma.HTTP.AdapterHelper
|
||||||
alias Pleroma.Pool.Connections
|
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do
|
||||||
connect_timeout: 5_000,
|
connect_timeout: 5_000,
|
||||||
domain_lookup_timeout: 5_000,
|
domain_lookup_timeout: 5_000,
|
||||||
tls_handshake_timeout: 5_000,
|
tls_handshake_timeout: 5_000,
|
||||||
retry: 1,
|
retry: 0,
|
||||||
retry_timeout: 1000,
|
retry_timeout: 1000,
|
||||||
await_up_timeout: 5_000
|
await_up_timeout: 5_000
|
||||||
]
|
]
|
||||||
|
@ -31,16 +31,7 @@ def options(incoming_opts \\ [], %URI{} = uri) do
|
||||||
|> Keyword.merge(config_opts)
|
|> Keyword.merge(config_opts)
|
||||||
|> add_scheme_opts(uri)
|
|> add_scheme_opts(uri)
|
||||||
|> AdapterHelper.maybe_add_proxy(proxy)
|
|> AdapterHelper.maybe_add_proxy(proxy)
|
||||||
|> maybe_get_conn(uri, incoming_opts)
|
|> Keyword.merge(incoming_opts)
|
||||||
end
|
|
||||||
|
|
||||||
@spec after_request(keyword()) :: :ok
|
|
||||||
def after_request(opts) do
|
|
||||||
if opts[:conn] && opts[:body_as] != :chunks do
|
|
||||||
Connections.checkout(opts[:conn], self(), :gun_connections)
|
|
||||||
end
|
|
||||||
|
|
||||||
:ok
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
|
defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
|
||||||
|
@ -48,30 +39,40 @@ defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
|
||||||
defp add_scheme_opts(opts, %{scheme: "https"}) do
|
defp add_scheme_opts(opts, %{scheme: "https"}) do
|
||||||
opts
|
opts
|
||||||
|> Keyword.put(:certificates_verification, true)
|
|> Keyword.put(:certificates_verification, true)
|
||||||
|> Keyword.put(:tls_opts, log_level: :warning)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_get_conn(adapter_opts, uri, incoming_opts) do
|
@spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()}
|
||||||
{receive_conn?, opts} =
|
def get_conn(uri, opts) do
|
||||||
adapter_opts
|
case ConnectionPool.get_conn(uri, opts) do
|
||||||
|> Keyword.merge(incoming_opts)
|
{:ok, conn_pid} -> {:ok, Keyword.merge(opts, conn: conn_pid, close_conn: false)}
|
||||||
|> Keyword.pop(:receive_conn, true)
|
err -> err
|
||||||
|
|
||||||
if Connections.alive?(:gun_connections) and receive_conn? do
|
|
||||||
checkin_conn(uri, opts)
|
|
||||||
else
|
|
||||||
opts
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp checkin_conn(uri, opts) do
|
@prefix Pleroma.Gun.ConnectionPool
|
||||||
case Connections.checkin(uri, :gun_connections) do
|
def limiter_setup do
|
||||||
nil ->
|
wait = Pleroma.Config.get([:connections_pool, :connection_acquisition_wait])
|
||||||
Task.start(Pleroma.Gun.Conn, :open, [uri, :gun_connections, opts])
|
retries = Pleroma.Config.get([:connections_pool, :connection_acquisition_retries])
|
||||||
opts
|
|
||||||
|
|
||||||
conn when is_pid(conn) ->
|
:pools
|
||||||
Keyword.merge(opts, conn: conn, close_conn: false)
|
|> Pleroma.Config.get([])
|
||||||
end
|
|> Enum.each(fn {name, opts} ->
|
||||||
|
max_running = Keyword.get(opts, :size, 50)
|
||||||
|
max_waiting = Keyword.get(opts, :max_waiting, 10)
|
||||||
|
|
||||||
|
result =
|
||||||
|
ConcurrentLimiter.new(:"#{@prefix}.#{name}", max_running, max_waiting,
|
||||||
|
wait: wait,
|
||||||
|
max_retries: retries
|
||||||
|
)
|
||||||
|
|
||||||
|
case result do
|
||||||
|
:ok -> :ok
|
||||||
|
{:error, :existing} -> :ok
|
||||||
|
e -> raise e
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,5 +24,6 @@ def options(connection_opts \\ [], %URI{} = uri) do
|
||||||
|
|
||||||
defp add_scheme_opts(opts, _), do: opts
|
defp add_scheme_opts(opts, _), do: opts
|
||||||
|
|
||||||
def after_request(_), do: :ok
|
@spec get_conn(URI.t(), keyword()) :: {:ok, keyword()}
|
||||||
|
def get_conn(_uri, opts), do: {:ok, opts}
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,124 +0,0 @@
|
||||||
# Pleroma: A lightweight social networking server
|
|
||||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
defmodule Pleroma.HTTP.Connection do
|
|
||||||
@moduledoc """
|
|
||||||
Configure Tesla.Client with default and customized adapter options.
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias Pleroma.Config
|
|
||||||
alias Pleroma.HTTP.AdapterHelper
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
@defaults [pool: :federation]
|
|
||||||
|
|
||||||
@type ip_address :: ipv4_address() | ipv6_address()
|
|
||||||
@type ipv4_address :: {0..255, 0..255, 0..255, 0..255}
|
|
||||||
@type ipv6_address ::
|
|
||||||
{0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535, 0..65_535}
|
|
||||||
@type proxy_type() :: :socks4 | :socks5
|
|
||||||
@type host() :: charlist() | ip_address()
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Merge default connection & adapter options with received ones.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@spec options(URI.t(), keyword()) :: keyword()
|
|
||||||
def options(%URI{} = uri, opts \\ []) do
|
|
||||||
@defaults
|
|
||||||
|> pool_timeout()
|
|
||||||
|> Keyword.merge(opts)
|
|
||||||
|> adapter_helper().options(uri)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp pool_timeout(opts) do
|
|
||||||
{config_key, default} =
|
|
||||||
if adapter() == Tesla.Adapter.Gun do
|
|
||||||
{:pools, Config.get([:pools, :default, :timeout])}
|
|
||||||
else
|
|
||||||
{:hackney_pools, 10_000}
|
|
||||||
end
|
|
||||||
|
|
||||||
timeout = Config.get([config_key, opts[:pool], :timeout], default)
|
|
||||||
|
|
||||||
Keyword.merge(opts, timeout: timeout)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec after_request(keyword()) :: :ok
|
|
||||||
def after_request(opts), do: adapter_helper().after_request(opts)
|
|
||||||
|
|
||||||
defp adapter, do: Application.get_env(:tesla, :adapter)
|
|
||||||
|
|
||||||
defp adapter_helper do
|
|
||||||
case adapter() do
|
|
||||||
Tesla.Adapter.Gun -> AdapterHelper.Gun
|
|
||||||
Tesla.Adapter.Hackney -> AdapterHelper.Hackney
|
|
||||||
_ -> AdapterHelper
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec parse_proxy(String.t() | tuple() | nil) ::
|
|
||||||
{:ok, host(), pos_integer()}
|
|
||||||
| {:ok, proxy_type(), host(), pos_integer()}
|
|
||||||
| {:error, atom()}
|
|
||||||
| nil
|
|
||||||
|
|
||||||
def parse_proxy(nil), do: nil
|
|
||||||
|
|
||||||
def parse_proxy(proxy) when is_binary(proxy) do
|
|
||||||
with [host, port] <- String.split(proxy, ":"),
|
|
||||||
{port, ""} <- Integer.parse(port) do
|
|
||||||
{:ok, parse_host(host), port}
|
|
||||||
else
|
|
||||||
{_, _} ->
|
|
||||||
Logger.warn("Parsing port failed #{inspect(proxy)}")
|
|
||||||
{:error, :invalid_proxy_port}
|
|
||||||
|
|
||||||
:error ->
|
|
||||||
Logger.warn("Parsing port failed #{inspect(proxy)}")
|
|
||||||
{:error, :invalid_proxy_port}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
|
|
||||||
{:error, :invalid_proxy}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_proxy(proxy) when is_tuple(proxy) do
|
|
||||||
with {type, host, port} <- proxy do
|
|
||||||
{:ok, type, parse_host(host), port}
|
|
||||||
else
|
|
||||||
_ ->
|
|
||||||
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
|
|
||||||
{:error, :invalid_proxy}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec parse_host(String.t() | atom() | charlist()) :: charlist() | ip_address()
|
|
||||||
def parse_host(host) when is_list(host), do: host
|
|
||||||
def parse_host(host) when is_atom(host), do: to_charlist(host)
|
|
||||||
|
|
||||||
def parse_host(host) when is_binary(host) do
|
|
||||||
host = to_charlist(host)
|
|
||||||
|
|
||||||
case :inet.parse_address(host) do
|
|
||||||
{:error, :einval} -> host
|
|
||||||
{:ok, ip} -> ip
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec format_host(String.t()) :: charlist()
|
|
||||||
def format_host(host) do
|
|
||||||
host_charlist = to_charlist(host)
|
|
||||||
|
|
||||||
case :inet.parse_address(host_charlist) do
|
|
||||||
{:error, :einval} ->
|
|
||||||
:idna.encode(host_charlist)
|
|
||||||
|
|
||||||
{:ok, _ip} ->
|
|
||||||
host_charlist
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -7,7 +7,7 @@ defmodule Pleroma.HTTP do
|
||||||
Wrapper for `Tesla.request/2`.
|
Wrapper for `Tesla.request/2`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Pleroma.HTTP.Connection
|
alias Pleroma.HTTP.AdapterHelper
|
||||||
alias Pleroma.HTTP.Request
|
alias Pleroma.HTTP.Request
|
||||||
alias Pleroma.HTTP.RequestBuilder, as: Builder
|
alias Pleroma.HTTP.RequestBuilder, as: Builder
|
||||||
alias Tesla.Client
|
alias Tesla.Client
|
||||||
|
@ -60,49 +60,30 @@ def post(url, body, headers \\ [], options \\ []),
|
||||||
{:ok, Env.t()} | {:error, any()}
|
{:ok, Env.t()} | {:error, any()}
|
||||||
def request(method, url, body, headers, options) when is_binary(url) do
|
def request(method, url, body, headers, options) when is_binary(url) do
|
||||||
uri = URI.parse(url)
|
uri = URI.parse(url)
|
||||||
adapter_opts = Connection.options(uri, options[:adapter] || [])
|
adapter_opts = AdapterHelper.options(uri, options[:adapter] || [])
|
||||||
options = put_in(options[:adapter], adapter_opts)
|
|
||||||
params = options[:params] || []
|
|
||||||
request = build_request(method, headers, options, url, body, params)
|
|
||||||
|
|
||||||
adapter = Application.get_env(:tesla, :adapter)
|
case AdapterHelper.get_conn(uri, adapter_opts) do
|
||||||
client = Tesla.client([Tesla.Middleware.FollowRedirects], adapter)
|
{:ok, adapter_opts} ->
|
||||||
|
options = put_in(options[:adapter], adapter_opts)
|
||||||
|
params = options[:params] || []
|
||||||
|
request = build_request(method, headers, options, url, body, params)
|
||||||
|
|
||||||
pid = Process.whereis(adapter_opts[:pool])
|
adapter = Application.get_env(:tesla, :adapter)
|
||||||
|
|
||||||
pool_alive? =
|
client = Tesla.client(adapter_middlewares(adapter), adapter)
|
||||||
if adapter == Tesla.Adapter.Gun && pid do
|
|
||||||
Process.alive?(pid)
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
request_opts =
|
maybe_limit(
|
||||||
adapter_opts
|
fn ->
|
||||||
|> Enum.into(%{})
|
request(client, request)
|
||||||
|> Map.put(:env, Pleroma.Config.get([:env]))
|
end,
|
||||||
|> Map.put(:pool_alive?, pool_alive?)
|
adapter,
|
||||||
|
adapter_opts
|
||||||
|
)
|
||||||
|
|
||||||
response = request(client, request, request_opts)
|
# Connection release is handled in a custom FollowRedirects middleware
|
||||||
|
err ->
|
||||||
Connection.after_request(adapter_opts)
|
err
|
||||||
|
end
|
||||||
response
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec request(Client.t(), keyword(), map()) :: {:ok, Env.t()} | {:error, any()}
|
|
||||||
def request(%Client{} = client, request, %{env: :test}), do: request(client, request)
|
|
||||||
|
|
||||||
def request(%Client{} = client, request, %{body_as: :chunks}), do: request(client, request)
|
|
||||||
|
|
||||||
def request(%Client{} = client, request, %{pool_alive?: false}), do: request(client, request)
|
|
||||||
|
|
||||||
def request(%Client{} = client, request, %{pool: pool, timeout: timeout}) do
|
|
||||||
:poolboy.transaction(
|
|
||||||
pool,
|
|
||||||
&Pleroma.Pool.Request.execute(&1, client, request, timeout),
|
|
||||||
timeout
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
|
@spec request(Client.t(), keyword()) :: {:ok, Env.t()} | {:error, any()}
|
||||||
|
@ -118,4 +99,19 @@ defp build_request(method, headers, options, url, body, params) do
|
||||||
|> Builder.add_param(:query, :query, params)
|
|> Builder.add_param(:query, :query, params)
|
||||||
|> Builder.convert_to_keyword()
|
|> Builder.convert_to_keyword()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@prefix Pleroma.Gun.ConnectionPool
|
||||||
|
defp maybe_limit(fun, Tesla.Adapter.Gun, opts) do
|
||||||
|
ConcurrentLimiter.limit(:"#{@prefix}.#{opts[:pool] || :default}", fun)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_limit(fun, _, _) do
|
||||||
|
fun.()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp adapter_middlewares(Tesla.Adapter.Gun) do
|
||||||
|
[Pleroma.HTTP.Middleware.FollowRedirects]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp adapter_middlewares(_), do: []
|
||||||
end
|
end
|
||||||
|
|
|
@ -409,6 +409,17 @@ def get_log_entry_message(%ModerationLog{
|
||||||
"@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
|
"@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||||
|
def get_log_entry_message(%ModerationLog{
|
||||||
|
data: %{
|
||||||
|
"actor" => %{"nickname" => actor_nickname},
|
||||||
|
"action" => "approve",
|
||||||
|
"subject" => users
|
||||||
|
}
|
||||||
|
}) do
|
||||||
|
"@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
|
||||||
|
end
|
||||||
|
|
||||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||||
def get_log_entry_message(%ModerationLog{
|
def get_log_entry_message(%ModerationLog{
|
||||||
data: %{
|
data: %{
|
||||||
|
|
|
@ -571,10 +571,7 @@ def skip?(%Activity{} = activity, %User{} = user) do
|
||||||
[
|
[
|
||||||
:self,
|
:self,
|
||||||
:invisible,
|
:invisible,
|
||||||
:followers,
|
:block_from_strangers,
|
||||||
:follows,
|
|
||||||
:non_followers,
|
|
||||||
:non_follows,
|
|
||||||
:recently_followed,
|
:recently_followed,
|
||||||
:filtered
|
:filtered
|
||||||
]
|
]
|
||||||
|
@ -595,45 +592,15 @@ def skip?(:invisible, %Activity{} = activity, _) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def skip?(
|
def skip?(
|
||||||
:followers,
|
:block_from_strangers,
|
||||||
%Activity{} = activity,
|
%Activity{} = activity,
|
||||||
%User{notification_settings: %{followers: false}} = user
|
%User{notification_settings: %{block_from_strangers: true}} = user
|
||||||
) do
|
|
||||||
actor = activity.data["actor"]
|
|
||||||
follower = User.get_cached_by_ap_id(actor)
|
|
||||||
User.following?(follower, user)
|
|
||||||
end
|
|
||||||
|
|
||||||
def skip?(
|
|
||||||
:non_followers,
|
|
||||||
%Activity{} = activity,
|
|
||||||
%User{notification_settings: %{non_followers: false}} = user
|
|
||||||
) do
|
) do
|
||||||
actor = activity.data["actor"]
|
actor = activity.data["actor"]
|
||||||
follower = User.get_cached_by_ap_id(actor)
|
follower = User.get_cached_by_ap_id(actor)
|
||||||
!User.following?(follower, user)
|
!User.following?(follower, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def skip?(
|
|
||||||
:follows,
|
|
||||||
%Activity{} = activity,
|
|
||||||
%User{notification_settings: %{follows: false}} = user
|
|
||||||
) do
|
|
||||||
actor = activity.data["actor"]
|
|
||||||
followed = User.get_cached_by_ap_id(actor)
|
|
||||||
User.following?(user, followed)
|
|
||||||
end
|
|
||||||
|
|
||||||
def skip?(
|
|
||||||
:non_follows,
|
|
||||||
%Activity{} = activity,
|
|
||||||
%User{notification_settings: %{non_follows: false}} = user
|
|
||||||
) do
|
|
||||||
actor = activity.data["actor"]
|
|
||||||
followed = User.get_cached_by_ap_id(actor)
|
|
||||||
!User.following?(user, followed)
|
|
||||||
end
|
|
||||||
|
|
||||||
# To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
|
# To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
|
||||||
def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
|
def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
|
||||||
actor = activity.data["actor"]
|
actor = activity.data["actor"]
|
||||||
|
|
|
@ -143,6 +143,10 @@ def fetch_object_from_id!(id, options \\ []) do
|
||||||
{:error, "Object has been deleted"} ->
|
{:error, "Object has been deleted"} ->
|
||||||
nil
|
nil
|
||||||
|
|
||||||
|
{:reject, reason} ->
|
||||||
|
Logger.info("Rejected #{id} while fetching: #{inspect(reason)}")
|
||||||
|
nil
|
||||||
|
|
||||||
e ->
|
e ->
|
||||||
Logger.error("Error while fetching #{id}: #{inspect(e)}")
|
Logger.error("Error while fetching #{id}: #{inspect(e)}")
|
||||||
nil
|
nil
|
||||||
|
|
54
lib/pleroma/plugs/frontend_static.ex
Normal file
54
lib/pleroma/plugs/frontend_static.ex
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Plugs.FrontendStatic do
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends.
|
||||||
|
"""
|
||||||
|
@behaviour Plug
|
||||||
|
|
||||||
|
def file_path(path, frontend_type \\ :primary) do
|
||||||
|
if configuration = Pleroma.Config.get([:frontends, frontend_type]) do
|
||||||
|
instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static")
|
||||||
|
|
||||||
|
Path.join([
|
||||||
|
instance_static_path,
|
||||||
|
"frontends",
|
||||||
|
configuration["name"],
|
||||||
|
configuration["ref"],
|
||||||
|
path
|
||||||
|
])
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(opts) do
|
||||||
|
opts
|
||||||
|
|> Keyword.put(:from, "__unconfigured_frontend_static_plug")
|
||||||
|
|> Plug.Static.init()
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(conn, opts) do
|
||||||
|
frontend_type = Map.get(opts, :frontend_type, :primary)
|
||||||
|
path = file_path("", frontend_type)
|
||||||
|
|
||||||
|
if path do
|
||||||
|
conn
|
||||||
|
|> call_static(opts, path)
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp call_static(conn, opts, from) do
|
||||||
|
opts =
|
||||||
|
opts
|
||||||
|
|> Map.put(:from, from)
|
||||||
|
|
||||||
|
Plug.Static.call(conn, opts)
|
||||||
|
end
|
||||||
|
end
|
|
@ -16,28 +16,24 @@ def file_path(path) do
|
||||||
instance_path =
|
instance_path =
|
||||||
Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path)
|
Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path)
|
||||||
|
|
||||||
if File.exists?(instance_path) do
|
frontend_path = Pleroma.Plugs.FrontendStatic.file_path(path, :primary)
|
||||||
instance_path
|
|
||||||
else
|
(File.exists?(instance_path) && instance_path) ||
|
||||||
|
(frontend_path && File.exists?(frontend_path) && frontend_path) ||
|
||||||
Path.join(Application.app_dir(:pleroma, "priv/static/"), path)
|
Path.join(Application.app_dir(:pleroma, "priv/static/"), path)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def init(opts) do
|
def init(opts) do
|
||||||
opts
|
opts
|
||||||
|> Keyword.put(:from, "__unconfigured_instance_static_plug")
|
|> Keyword.put(:from, "__unconfigured_instance_static_plug")
|
||||||
|> Keyword.put(:at, "/__unconfigured_instance_static_plug")
|
|
||||||
|> Plug.Static.init()
|
|> Plug.Static.init()
|
||||||
end
|
end
|
||||||
|
|
||||||
for only <- Pleroma.Constants.static_only_files() do
|
for only <- Pleroma.Constants.static_only_files() do
|
||||||
at = Plug.Router.Utils.split("/")
|
|
||||||
|
|
||||||
def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
|
def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
|
||||||
call_static(
|
call_static(
|
||||||
conn,
|
conn,
|
||||||
opts,
|
opts,
|
||||||
unquote(at),
|
|
||||||
Pleroma.Config.get([:instance, :static_dir], "instance/static")
|
Pleroma.Config.get([:instance, :static_dir], "instance/static")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -47,11 +43,10 @@ def call(conn, _) do
|
||||||
conn
|
conn
|
||||||
end
|
end
|
||||||
|
|
||||||
defp call_static(conn, opts, at, from) do
|
defp call_static(conn, opts, from) do
|
||||||
opts =
|
opts =
|
||||||
opts
|
opts
|
||||||
|> Map.put(:from, from)
|
|> Map.put(:from, from)
|
||||||
|> Map.put(:at, at)
|
|
||||||
|
|
||||||
Plug.Static.call(conn, opts)
|
Plug.Static.call(conn, opts)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,283 +0,0 @@
|
||||||
# Pleroma: A lightweight social networking server
|
|
||||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
defmodule Pleroma.Pool.Connections do
|
|
||||||
use GenServer
|
|
||||||
|
|
||||||
alias Pleroma.Config
|
|
||||||
alias Pleroma.Gun
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
@type domain :: String.t()
|
|
||||||
@type conn :: Pleroma.Gun.Conn.t()
|
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
|
||||||
conns: %{domain() => conn()},
|
|
||||||
opts: keyword()
|
|
||||||
}
|
|
||||||
|
|
||||||
defstruct conns: %{}, opts: []
|
|
||||||
|
|
||||||
@spec start_link({atom(), keyword()}) :: {:ok, pid()}
|
|
||||||
def start_link({name, opts}) do
|
|
||||||
GenServer.start_link(__MODULE__, opts, name: name)
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init(opts), do: {:ok, %__MODULE__{conns: %{}, opts: opts}}
|
|
||||||
|
|
||||||
@spec checkin(String.t() | URI.t(), atom()) :: pid() | nil
|
|
||||||
def checkin(url, name)
|
|
||||||
def checkin(url, name) when is_binary(url), do: checkin(URI.parse(url), name)
|
|
||||||
|
|
||||||
def checkin(%URI{} = uri, name) do
|
|
||||||
timeout = Config.get([:connections_pool, :checkin_timeout], 250)
|
|
||||||
|
|
||||||
GenServer.call(name, {:checkin, uri}, timeout)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec alive?(atom()) :: boolean()
|
|
||||||
def alive?(name) do
|
|
||||||
if pid = Process.whereis(name) do
|
|
||||||
Process.alive?(pid)
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec get_state(atom()) :: t()
|
|
||||||
def get_state(name) do
|
|
||||||
GenServer.call(name, :state)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec count(atom()) :: pos_integer()
|
|
||||||
def count(name) do
|
|
||||||
GenServer.call(name, :count)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec get_unused_conns(atom()) :: [{domain(), conn()}]
|
|
||||||
def get_unused_conns(name) do
|
|
||||||
GenServer.call(name, :unused_conns)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec checkout(pid(), pid(), atom()) :: :ok
|
|
||||||
def checkout(conn, pid, name) do
|
|
||||||
GenServer.cast(name, {:checkout, conn, pid})
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec add_conn(atom(), String.t(), Pleroma.Gun.Conn.t()) :: :ok
|
|
||||||
def add_conn(name, key, conn) do
|
|
||||||
GenServer.cast(name, {:add_conn, key, conn})
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec remove_conn(atom(), String.t()) :: :ok
|
|
||||||
def remove_conn(name, key) do
|
|
||||||
GenServer.cast(name, {:remove_conn, key})
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_cast({:add_conn, key, conn}, state) do
|
|
||||||
state = put_in(state.conns[key], conn)
|
|
||||||
|
|
||||||
Process.monitor(conn.conn)
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_cast({:checkout, conn_pid, pid}, state) do
|
|
||||||
state =
|
|
||||||
with true <- Process.alive?(conn_pid),
|
|
||||||
{key, conn} <- find_conn(state.conns, conn_pid),
|
|
||||||
used_by <- List.keydelete(conn.used_by, pid, 0) do
|
|
||||||
conn_state = if used_by == [], do: :idle, else: conn.conn_state
|
|
||||||
|
|
||||||
put_in(state.conns[key], %{conn | conn_state: conn_state, used_by: used_by})
|
|
||||||
else
|
|
||||||
false ->
|
|
||||||
Logger.debug("checkout for closed conn #{inspect(conn_pid)}")
|
|
||||||
state
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
Logger.debug("checkout for alive conn #{inspect(conn_pid)}, but is not in state")
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_cast({:remove_conn, key}, state) do
|
|
||||||
state = put_in(state.conns, Map.delete(state.conns, key))
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call({:checkin, uri}, from, state) do
|
|
||||||
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
|
|
||||||
|
|
||||||
case state.conns[key] do
|
|
||||||
%{conn: pid, gun_state: :up} = conn ->
|
|
||||||
time = :os.system_time(:second)
|
|
||||||
last_reference = time - conn.last_reference
|
|
||||||
crf = crf(last_reference, 100, conn.crf)
|
|
||||||
|
|
||||||
state =
|
|
||||||
put_in(state.conns[key], %{
|
|
||||||
conn
|
|
||||||
| last_reference: time,
|
|
||||||
crf: crf,
|
|
||||||
conn_state: :active,
|
|
||||||
used_by: [from | conn.used_by]
|
|
||||||
})
|
|
||||||
|
|
||||||
{:reply, pid, state}
|
|
||||||
|
|
||||||
%{gun_state: :down} ->
|
|
||||||
{:reply, nil, state}
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
{:reply, nil, state}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call(:state, _from, state), do: {:reply, state, state}
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call(:count, _from, state) do
|
|
||||||
{:reply, Enum.count(state.conns), state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call(:unused_conns, _from, state) do
|
|
||||||
unused_conns =
|
|
||||||
state.conns
|
|
||||||
|> Enum.filter(&filter_conns/1)
|
|
||||||
|> Enum.sort(&sort_conns/2)
|
|
||||||
|
|
||||||
{:reply, unused_conns, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp filter_conns({_, %{conn_state: :idle, used_by: []}}), do: true
|
|
||||||
defp filter_conns(_), do: false
|
|
||||||
|
|
||||||
defp sort_conns({_, c1}, {_, c2}) do
|
|
||||||
c1.crf <= c2.crf and c1.last_reference <= c2.last_reference
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_up, conn_pid, _protocol}, state) do
|
|
||||||
%{origin_host: host, origin_scheme: scheme, origin_port: port} = Gun.info(conn_pid)
|
|
||||||
|
|
||||||
host =
|
|
||||||
case :inet.ntoa(host) do
|
|
||||||
{:error, :einval} -> host
|
|
||||||
ip -> ip
|
|
||||||
end
|
|
||||||
|
|
||||||
key = "#{scheme}:#{host}:#{port}"
|
|
||||||
|
|
||||||
state =
|
|
||||||
with {key, conn} <- find_conn(state.conns, conn_pid, key),
|
|
||||||
{true, key} <- {Process.alive?(conn_pid), key} do
|
|
||||||
put_in(state.conns[key], %{
|
|
||||||
conn
|
|
||||||
| gun_state: :up,
|
|
||||||
conn_state: :active,
|
|
||||||
retries: 0
|
|
||||||
})
|
|
||||||
else
|
|
||||||
{false, key} ->
|
|
||||||
put_in(
|
|
||||||
state.conns,
|
|
||||||
Map.delete(state.conns, key)
|
|
||||||
)
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
:ok = Gun.close(conn_pid)
|
|
||||||
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do
|
|
||||||
retries = Config.get([:connections_pool, :retry], 1)
|
|
||||||
# we can't get info on this pid, because pid is dead
|
|
||||||
state =
|
|
||||||
with {key, conn} <- find_conn(state.conns, conn_pid),
|
|
||||||
{true, key} <- {Process.alive?(conn_pid), key} do
|
|
||||||
if conn.retries == retries do
|
|
||||||
:ok = Gun.close(conn.conn)
|
|
||||||
|
|
||||||
put_in(
|
|
||||||
state.conns,
|
|
||||||
Map.delete(state.conns, key)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
put_in(state.conns[key], %{
|
|
||||||
conn
|
|
||||||
| gun_state: :down,
|
|
||||||
retries: conn.retries + 1
|
|
||||||
})
|
|
||||||
end
|
|
||||||
else
|
|
||||||
{false, key} ->
|
|
||||||
put_in(
|
|
||||||
state.conns,
|
|
||||||
Map.delete(state.conns, key)
|
|
||||||
)
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
Logger.debug(":gun_down for conn which isn't found in state")
|
|
||||||
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do
|
|
||||||
Logger.debug("received DOWN message for #{inspect(conn_pid)} reason -> #{inspect(reason)}")
|
|
||||||
|
|
||||||
state =
|
|
||||||
with {key, conn} <- find_conn(state.conns, conn_pid) do
|
|
||||||
Enum.each(conn.used_by, fn {pid, _ref} ->
|
|
||||||
Process.exit(pid, reason)
|
|
||||||
end)
|
|
||||||
|
|
||||||
put_in(
|
|
||||||
state.conns,
|
|
||||||
Map.delete(state.conns, key)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
nil ->
|
|
||||||
Logger.debug(":DOWN for conn which isn't found in state")
|
|
||||||
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp find_conn(conns, conn_pid) do
|
|
||||||
Enum.find(conns, fn {_key, conn} ->
|
|
||||||
conn.conn == conn_pid
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp find_conn(conns, conn_pid, conn_key) do
|
|
||||||
Enum.find(conns, fn {key, conn} ->
|
|
||||||
key == conn_key and conn.conn == conn_pid
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
def crf(current, steps, crf) do
|
|
||||||
1 + :math.pow(0.5, current / steps) * crf
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,22 +0,0 @@
|
||||||
# Pleroma: A lightweight social networking server
|
|
||||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
defmodule Pleroma.Pool do
|
|
||||||
def child_spec(opts) do
|
|
||||||
poolboy_opts =
|
|
||||||
opts
|
|
||||||
|> Keyword.put(:worker_module, Pleroma.Pool.Request)
|
|
||||||
|> Keyword.put(:name, {:local, opts[:name]})
|
|
||||||
|> Keyword.put(:size, opts[:size])
|
|
||||||
|> Keyword.put(:max_overflow, opts[:max_overflow])
|
|
||||||
|
|
||||||
%{
|
|
||||||
id: opts[:id] || {__MODULE__, make_ref()},
|
|
||||||
start: {:poolboy, :start_link, [poolboy_opts, [name: opts[:name]]]},
|
|
||||||
restart: :permanent,
|
|
||||||
shutdown: 5000,
|
|
||||||
type: :worker
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,65 +0,0 @@
|
||||||
# Pleroma: A lightweight social networking server
|
|
||||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
defmodule Pleroma.Pool.Request do
|
|
||||||
use GenServer
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
def start_link(args) do
|
|
||||||
GenServer.start_link(__MODULE__, args)
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init(_), do: {:ok, []}
|
|
||||||
|
|
||||||
@spec execute(pid() | atom(), Tesla.Client.t(), keyword(), pos_integer()) ::
|
|
||||||
{:ok, Tesla.Env.t()} | {:error, any()}
|
|
||||||
def execute(pid, client, request, timeout) do
|
|
||||||
GenServer.call(pid, {:execute, client, request}, timeout)
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call({:execute, client, request}, _from, state) do
|
|
||||||
response = Pleroma.HTTP.request(client, request)
|
|
||||||
|
|
||||||
{:reply, response, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_data, _conn, _stream, _, _}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_up, _conn, _protocol}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_down, _conn, _protocol, _reason, _killed}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_error, _conn, _stream, _error}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_push, _conn, _stream, _new_stream, _method, _uri, _headers}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:gun_response, _conn, _stream, _, _status, _headers}, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info(msg, state) do
|
|
||||||
Logger.warn("Received unexpected message #{inspect(__MODULE__)} #{inspect(msg)}")
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,42 +0,0 @@
|
||||||
# Pleroma: A lightweight social networking server
|
|
||||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
defmodule Pleroma.Pool.Supervisor do
|
|
||||||
use Supervisor
|
|
||||||
|
|
||||||
alias Pleroma.Config
|
|
||||||
alias Pleroma.Pool
|
|
||||||
|
|
||||||
def start_link(args) do
|
|
||||||
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
|
|
||||||
end
|
|
||||||
|
|
||||||
def init(_) do
|
|
||||||
conns_child = %{
|
|
||||||
id: Pool.Connections,
|
|
||||||
start:
|
|
||||||
{Pool.Connections, :start_link, [{:gun_connections, Config.get([:connections_pool])}]}
|
|
||||||
}
|
|
||||||
|
|
||||||
Supervisor.init([conns_child | pools()], strategy: :one_for_one)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp pools do
|
|
||||||
pools = Config.get(:pools)
|
|
||||||
|
|
||||||
pools =
|
|
||||||
if Config.get([Pleroma.Upload, :proxy_remote]) == false do
|
|
||||||
Keyword.delete(pools, :upload)
|
|
||||||
else
|
|
||||||
pools
|
|
||||||
end
|
|
||||||
|
|
||||||
for {pool_name, pool_opts} <- pools do
|
|
||||||
pool_opts
|
|
||||||
|> Keyword.put(:id, {Pool, pool_name})
|
|
||||||
|> Keyword.put(:name, pool_name)
|
|
||||||
|> Pool.child_spec()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -5,6 +5,8 @@
|
||||||
defmodule Pleroma.ReverseProxy.Client.Tesla do
|
defmodule Pleroma.ReverseProxy.Client.Tesla do
|
||||||
@behaviour Pleroma.ReverseProxy.Client
|
@behaviour Pleroma.ReverseProxy.Client
|
||||||
|
|
||||||
|
alias Pleroma.Gun.ConnectionPool
|
||||||
|
|
||||||
@type headers() :: [{String.t(), String.t()}]
|
@type headers() :: [{String.t(), String.t()}]
|
||||||
@type status() :: pos_integer()
|
@type status() :: pos_integer()
|
||||||
|
|
||||||
|
@ -31,6 +33,8 @@ def request(method, url, headers, body, opts \\ []) do
|
||||||
if is_map(response.body) and method != :head do
|
if is_map(response.body) and method != :head do
|
||||||
{:ok, response.status, response.headers, response.body}
|
{:ok, response.status, response.headers, response.body}
|
||||||
else
|
else
|
||||||
|
conn_pid = response.opts[:adapter][:conn]
|
||||||
|
ConnectionPool.release_conn(conn_pid)
|
||||||
{:ok, response.status, response.headers}
|
{:ok, response.status, response.headers}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
@ -41,15 +45,8 @@ def request(method, url, headers, body, opts \\ []) do
|
||||||
@impl true
|
@impl true
|
||||||
@spec stream_body(map()) ::
|
@spec stream_body(map()) ::
|
||||||
{:ok, binary(), map()} | {:error, atom() | String.t()} | :done | no_return()
|
{:ok, binary(), map()} | {:error, atom() | String.t()} | :done | no_return()
|
||||||
def stream_body(%{pid: pid, opts: opts, fin: true}) do
|
def stream_body(%{pid: pid, fin: true}) do
|
||||||
# if connection was reused, but in tesla were redirects,
|
ConnectionPool.release_conn(pid)
|
||||||
# tesla returns new opened connection, which must be closed manually
|
|
||||||
if opts[:old_conn], do: Tesla.Adapter.Gun.close(pid)
|
|
||||||
# if there were redirects we need to checkout old conn
|
|
||||||
conn = opts[:old_conn] || opts[:conn]
|
|
||||||
|
|
||||||
if conn, do: :ok = Pleroma.Pool.Connections.checkout(conn, self(), :gun_connections)
|
|
||||||
|
|
||||||
:done
|
:done
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -74,8 +71,7 @@ defp read_chunk!(%{pid: pid, stream: stream, opts: opts}) do
|
||||||
@impl true
|
@impl true
|
||||||
@spec close(map) :: :ok | no_return()
|
@spec close(map) :: :ok | no_return()
|
||||||
def close(%{pid: pid}) do
|
def close(%{pid: pid}) do
|
||||||
adapter = check_adapter()
|
ConnectionPool.release_conn(pid)
|
||||||
adapter.close(pid)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp check_adapter do
|
defp check_adapter do
|
||||||
|
|
|
@ -165,6 +165,9 @@ defp request(method, url, headers, opts) do
|
||||||
{:ok, code, _, _} ->
|
{:ok, code, _, _} ->
|
||||||
{:error, {:invalid_http_response, code}}
|
{:error, {:invalid_http_response, code}}
|
||||||
|
|
||||||
|
{:ok, code, _} ->
|
||||||
|
{:error, {:invalid_http_response, code}}
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
{:error, error}
|
{:error, error}
|
||||||
end
|
end
|
||||||
|
|
76
lib/pleroma/telemetry/logger.ex
Normal file
76
lib/pleroma/telemetry/logger.ex
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
defmodule Pleroma.Telemetry.Logger do
|
||||||
|
@moduledoc "Transforms Pleroma telemetry events to logs"
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@events [
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :start],
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :stop],
|
||||||
|
[:pleroma, :connection_pool, :provision_failure],
|
||||||
|
[:pleroma, :connection_pool, :client_death]
|
||||||
|
]
|
||||||
|
def attach do
|
||||||
|
:telemetry.attach_many("pleroma-logger", @events, &handle_event/4, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Passing anonymous functions instead of strings to logger is intentional,
|
||||||
|
# that way strings won't be concatenated if the message is going to be thrown
|
||||||
|
# out anyway due to higher log level configured
|
||||||
|
|
||||||
|
def handle_event(
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :start],
|
||||||
|
_,
|
||||||
|
%{max_connections: max_connections, reclaim_max: reclaim_max},
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
Logger.debug(fn ->
|
||||||
|
"Connection pool is exhausted (reached #{max_connections} connections). Starting idle connection cleanup to reclaim as much as #{
|
||||||
|
reclaim_max
|
||||||
|
} connections"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event(
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :stop],
|
||||||
|
%{reclaimed_count: 0},
|
||||||
|
_,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
Logger.error(fn ->
|
||||||
|
"Connection pool failed to reclaim any connections due to all of them being in use. It will have to drop requests for opening connections to new hosts"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event(
|
||||||
|
[:pleroma, :connection_pool, :reclaim, :stop],
|
||||||
|
%{reclaimed_count: reclaimed_count},
|
||||||
|
_,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
Logger.debug(fn -> "Connection pool cleaned up #{reclaimed_count} idle connections" end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event(
|
||||||
|
[:pleroma, :connection_pool, :provision_failure],
|
||||||
|
%{opts: [key | _]},
|
||||||
|
_,
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
Logger.error(fn ->
|
||||||
|
"Connection pool had to refuse opening a connection to #{key} due to connection limit exhaustion"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event(
|
||||||
|
[:pleroma, :connection_pool, :client_death],
|
||||||
|
%{client_pid: client_pid, reason: reason},
|
||||||
|
%{key: key},
|
||||||
|
_
|
||||||
|
) do
|
||||||
|
Logger.warn(fn ->
|
||||||
|
"Pool worker for #{key}: Client #{inspect(client_pid)} died before releasing the connection with #{
|
||||||
|
inspect(reason)
|
||||||
|
}"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
110
lib/pleroma/tesla/middleware/follow_redirects.ex
Normal file
110
lib/pleroma/tesla/middleware/follow_redirects.ex
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2015-2020 Tymon Tobolski <https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex>
|
||||||
|
# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.HTTP.Middleware.FollowRedirects do
|
||||||
|
@moduledoc """
|
||||||
|
Pool-aware version of https://github.com/teamon/tesla/blob/master/lib/tesla/middleware/follow_redirects.ex
|
||||||
|
|
||||||
|
Follow 3xx redirects
|
||||||
|
## Options
|
||||||
|
- `:max_redirects` - limit number of redirects (default: `5`)
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pleroma.Gun.ConnectionPool
|
||||||
|
|
||||||
|
@behaviour Tesla.Middleware
|
||||||
|
|
||||||
|
@max_redirects 5
|
||||||
|
@redirect_statuses [301, 302, 303, 307, 308]
|
||||||
|
|
||||||
|
@impl Tesla.Middleware
|
||||||
|
def call(env, next, opts \\ []) do
|
||||||
|
max = Keyword.get(opts, :max_redirects, @max_redirects)
|
||||||
|
|
||||||
|
redirect(env, next, max)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp redirect(env, next, left) do
|
||||||
|
opts = env.opts[:adapter]
|
||||||
|
|
||||||
|
case Tesla.run(env, next) do
|
||||||
|
{:ok, %{status: status} = res} when status in @redirect_statuses and left > 0 ->
|
||||||
|
release_conn(opts)
|
||||||
|
|
||||||
|
case Tesla.get_header(res, "location") do
|
||||||
|
nil ->
|
||||||
|
{:ok, res}
|
||||||
|
|
||||||
|
location ->
|
||||||
|
location = parse_location(location, res)
|
||||||
|
|
||||||
|
case get_conn(location, opts) do
|
||||||
|
{:ok, opts} ->
|
||||||
|
%{env | opts: Keyword.put(env.opts, :adapter, opts)}
|
||||||
|
|> new_request(res.status, location)
|
||||||
|
|> redirect(next, left - 1)
|
||||||
|
|
||||||
|
e ->
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, %{status: status}} when status in @redirect_statuses ->
|
||||||
|
release_conn(opts)
|
||||||
|
{:error, {__MODULE__, :too_many_redirects}}
|
||||||
|
|
||||||
|
{:error, _} = e ->
|
||||||
|
release_conn(opts)
|
||||||
|
e
|
||||||
|
|
||||||
|
other ->
|
||||||
|
unless opts[:body_as] == :chunks do
|
||||||
|
release_conn(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
other
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_conn(location, opts) do
|
||||||
|
uri = URI.parse(location)
|
||||||
|
|
||||||
|
case ConnectionPool.get_conn(uri, opts) do
|
||||||
|
{:ok, conn} ->
|
||||||
|
{:ok, Keyword.merge(opts, conn: conn)}
|
||||||
|
|
||||||
|
e ->
|
||||||
|
e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp release_conn(opts) do
|
||||||
|
ConnectionPool.release_conn(opts[:conn])
|
||||||
|
end
|
||||||
|
|
||||||
|
# The 303 (See Other) redirect was added in HTTP/1.1 to indicate that the originally
|
||||||
|
# requested resource is not available, however a related resource (or another redirect)
|
||||||
|
# available via GET is available at the specified location.
|
||||||
|
# https://tools.ietf.org/html/rfc7231#section-6.4.4
|
||||||
|
defp new_request(env, 303, location), do: %{env | url: location, method: :get, query: []}
|
||||||
|
|
||||||
|
# The 307 (Temporary Redirect) status code indicates that the target
|
||||||
|
# resource resides temporarily under a different URI and the user agent
|
||||||
|
# MUST NOT change the request method (...)
|
||||||
|
# https://tools.ietf.org/html/rfc7231#section-6.4.7
|
||||||
|
defp new_request(env, 307, location), do: %{env | url: location}
|
||||||
|
|
||||||
|
defp new_request(env, _, location), do: %{env | url: location, query: []}
|
||||||
|
|
||||||
|
defp parse_location("https://" <> _rest = location, _env), do: location
|
||||||
|
defp parse_location("http://" <> _rest = location, _env), do: location
|
||||||
|
|
||||||
|
defp parse_location(location, env) do
|
||||||
|
env.url
|
||||||
|
|> URI.parse()
|
||||||
|
|> URI.merge(location)
|
||||||
|
|> URI.to_string()
|
||||||
|
end
|
||||||
|
end
|
|
@ -42,7 +42,12 @@ defmodule Pleroma.User do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@type t :: %__MODULE__{}
|
@type t :: %__MODULE__{}
|
||||||
@type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
|
@type account_status ::
|
||||||
|
:active
|
||||||
|
| :deactivated
|
||||||
|
| :password_reset_pending
|
||||||
|
| :confirmation_pending
|
||||||
|
| :approval_pending
|
||||||
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
|
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
|
||||||
|
|
||||||
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
|
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
|
||||||
|
@ -106,6 +111,8 @@ defmodule Pleroma.User do
|
||||||
field(:locked, :boolean, default: false)
|
field(:locked, :boolean, default: false)
|
||||||
field(:confirmation_pending, :boolean, default: false)
|
field(:confirmation_pending, :boolean, default: false)
|
||||||
field(:password_reset_pending, :boolean, default: false)
|
field(:password_reset_pending, :boolean, default: false)
|
||||||
|
field(:approval_pending, :boolean, default: false)
|
||||||
|
field(:registration_reason, :string, default: nil)
|
||||||
field(:confirmation_token, :string, default: nil)
|
field(:confirmation_token, :string, default: nil)
|
||||||
field(:default_scope, :string, default: "public")
|
field(:default_scope, :string, default: "public")
|
||||||
field(:domain_blocks, {:array, :string}, default: [])
|
field(:domain_blocks, {:array, :string}, default: [])
|
||||||
|
@ -262,6 +269,7 @@ def binary_id(%User{} = user), do: binary_id(user.id)
|
||||||
@spec account_status(User.t()) :: account_status()
|
@spec account_status(User.t()) :: account_status()
|
||||||
def account_status(%User{deactivated: true}), do: :deactivated
|
def account_status(%User{deactivated: true}), do: :deactivated
|
||||||
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
|
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
|
||||||
|
def account_status(%User{approval_pending: true}), do: :approval_pending
|
||||||
|
|
||||||
def account_status(%User{confirmation_pending: true}) do
|
def account_status(%User{confirmation_pending: true}) do
|
||||||
if Config.get([:instance, :account_activation_required]) do
|
if Config.get([:instance, :account_activation_required]) do
|
||||||
|
@ -633,6 +641,7 @@ def force_password_reset(user), do: update_password_reset_pending(user, true)
|
||||||
def register_changeset(struct, params \\ %{}, opts \\ []) do
|
def register_changeset(struct, params \\ %{}, opts \\ []) do
|
||||||
bio_limit = Config.get([:instance, :user_bio_length], 5000)
|
bio_limit = Config.get([:instance, :user_bio_length], 5000)
|
||||||
name_limit = Config.get([:instance, :user_name_length], 100)
|
name_limit = Config.get([:instance, :user_name_length], 100)
|
||||||
|
reason_limit = Config.get([:instance, :registration_reason_length], 500)
|
||||||
params = Map.put_new(params, :accepts_chat_messages, true)
|
params = Map.put_new(params, :accepts_chat_messages, true)
|
||||||
|
|
||||||
need_confirmation? =
|
need_confirmation? =
|
||||||
|
@ -642,8 +651,16 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|
||||||
opts[:need_confirmation]
|
opts[:need_confirmation]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
need_approval? =
|
||||||
|
if is_nil(opts[:need_approval]) do
|
||||||
|
Config.get([:instance, :account_approval_required])
|
||||||
|
else
|
||||||
|
opts[:need_approval]
|
||||||
|
end
|
||||||
|
|
||||||
struct
|
struct
|
||||||
|> confirmation_changeset(need_confirmation: need_confirmation?)
|
|> confirmation_changeset(need_confirmation: need_confirmation?)
|
||||||
|
|> approval_changeset(need_approval: need_approval?)
|
||||||
|> cast(params, [
|
|> cast(params, [
|
||||||
:bio,
|
:bio,
|
||||||
:raw_bio,
|
:raw_bio,
|
||||||
|
@ -653,7 +670,8 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|
||||||
:password,
|
:password,
|
||||||
:password_confirmation,
|
:password_confirmation,
|
||||||
:emoji,
|
:emoji,
|
||||||
:accepts_chat_messages
|
:accepts_chat_messages,
|
||||||
|
:registration_reason
|
||||||
])
|
])
|
||||||
|> validate_required([:name, :nickname, :password, :password_confirmation])
|
|> validate_required([:name, :nickname, :password, :password_confirmation])
|
||||||
|> validate_confirmation(:password)
|
|> validate_confirmation(:password)
|
||||||
|
@ -664,6 +682,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|
||||||
|> validate_format(:email, @email_regex)
|
|> validate_format(:email, @email_regex)
|
||||||
|> validate_length(:bio, max: bio_limit)
|
|> validate_length(:bio, max: bio_limit)
|
||||||
|> validate_length(:name, min: 1, max: name_limit)
|
|> validate_length(:name, min: 1, max: name_limit)
|
||||||
|
|> validate_length(:registration_reason, max: reason_limit)
|
||||||
|> maybe_validate_required_email(opts[:external])
|
|> maybe_validate_required_email(opts[:external])
|
||||||
|> put_password_hash
|
|> put_password_hash
|
||||||
|> put_ap_id()
|
|> put_ap_id()
|
||||||
|
@ -713,27 +732,52 @@ def register(%Ecto.Changeset{} = changeset) do
|
||||||
def post_register_action(%User{} = user) do
|
def post_register_action(%User{} = user) do
|
||||||
with {:ok, user} <- autofollow_users(user),
|
with {:ok, user} <- autofollow_users(user),
|
||||||
{:ok, user} <- set_cache(user),
|
{:ok, user} <- set_cache(user),
|
||||||
{:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
|
{:ok, _} <- send_welcome_email(user),
|
||||||
|
{:ok, _} <- send_welcome_message(user),
|
||||||
{:ok, _} <- try_send_confirmation_email(user) do
|
{:ok, _} <- try_send_confirmation_email(user) do
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def try_send_confirmation_email(%User{} = user) do
|
def send_welcome_message(user) do
|
||||||
if user.confirmation_pending &&
|
if User.WelcomeMessage.enabled?() do
|
||||||
Config.get([:instance, :account_activation_required]) do
|
User.WelcomeMessage.post_message(user)
|
||||||
user
|
|
||||||
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|
|
||||||
|> Pleroma.Emails.Mailer.deliver_async()
|
|
||||||
|
|
||||||
{:ok, :enqueued}
|
{:ok, :enqueued}
|
||||||
else
|
else
|
||||||
{:ok, :noop}
|
{:ok, :noop}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def try_send_confirmation_email(users) do
|
def send_welcome_email(%User{email: email} = user) when is_binary(email) do
|
||||||
Enum.each(users, &try_send_confirmation_email/1)
|
if User.WelcomeEmail.enabled?() do
|
||||||
|
User.WelcomeEmail.send_email(user)
|
||||||
|
{:ok, :enqueued}
|
||||||
|
else
|
||||||
|
{:ok, :noop}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_welcome_email(_), do: {:ok, :noop}
|
||||||
|
|
||||||
|
@spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
|
||||||
|
def try_send_confirmation_email(%User{confirmation_pending: true} = user) do
|
||||||
|
if Config.get([:instance, :account_activation_required]) do
|
||||||
|
send_confirmation_email(user)
|
||||||
|
{:ok, :enqueued}
|
||||||
|
else
|
||||||
|
{:ok, :noop}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def try_send_confirmation_email(_), do: {:ok, :noop}
|
||||||
|
|
||||||
|
@spec send_confirmation_email(Uset.t()) :: User.t()
|
||||||
|
def send_confirmation_email(%User{} = user) do
|
||||||
|
user
|
||||||
|
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|
||||||
|
|> Pleroma.Emails.Mailer.deliver_async()
|
||||||
|
|
||||||
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
def needs_update?(%User{local: true}), do: false
|
def needs_update?(%User{local: true}), do: false
|
||||||
|
@ -1469,6 +1513,19 @@ def deactivate(%User{} = user, status) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def approve(users) when is_list(users) do
|
||||||
|
Repo.transaction(fn ->
|
||||||
|
Enum.map(users, fn user ->
|
||||||
|
with {:ok, user} <- approve(user), do: user
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def approve(%User{} = user) do
|
||||||
|
change(user, approval_pending: false)
|
||||||
|
|> update_and_set_cache()
|
||||||
|
end
|
||||||
|
|
||||||
def update_notification_settings(%User{} = user, settings) do
|
def update_notification_settings(%User{} = user, settings) do
|
||||||
user
|
user
|
||||||
|> cast(%{notification_settings: settings}, [])
|
|> cast(%{notification_settings: settings}, [])
|
||||||
|
@ -1495,12 +1552,17 @@ defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate
|
||||||
defp delete_or_deactivate(%User{local: true} = user) do
|
defp delete_or_deactivate(%User{local: true} = user) do
|
||||||
status = account_status(user)
|
status = account_status(user)
|
||||||
|
|
||||||
if status == :confirmation_pending do
|
case status do
|
||||||
delete_and_invalidate_cache(user)
|
:confirmation_pending ->
|
||||||
else
|
delete_and_invalidate_cache(user)
|
||||||
user
|
|
||||||
|> change(%{deactivated: true, email: nil})
|
:approval_pending ->
|
||||||
|> update_and_set_cache()
|
delete_and_invalidate_cache(user)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
user
|
||||||
|
|> change(%{deactivated: true, email: nil})
|
||||||
|
|> update_and_set_cache()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2153,6 +2215,12 @@ def confirmation_changeset(user, need_confirmation: need_confirmation?) do
|
||||||
cast(user, params, [:confirmation_pending, :confirmation_token])
|
cast(user, params, [:confirmation_pending, :confirmation_token])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec approval_changeset(User.t(), keyword()) :: Changeset.t()
|
||||||
|
def approval_changeset(user, need_approval: need_approval?) do
|
||||||
|
params = if need_approval?, do: %{approval_pending: true}, else: %{approval_pending: false}
|
||||||
|
cast(user, params, [:approval_pending])
|
||||||
|
end
|
||||||
|
|
||||||
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
|
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
|
||||||
if id not in user.pinned_activities do
|
if id not in user.pinned_activities do
|
||||||
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
|
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
|
||||||
|
|
|
@ -10,21 +10,15 @@ defmodule Pleroma.User.NotificationSetting do
|
||||||
@primary_key false
|
@primary_key false
|
||||||
|
|
||||||
embedded_schema do
|
embedded_schema do
|
||||||
field(:followers, :boolean, default: true)
|
field(:block_from_strangers, :boolean, default: false)
|
||||||
field(:follows, :boolean, default: true)
|
field(:hide_notification_contents, :boolean, default: false)
|
||||||
field(:non_follows, :boolean, default: true)
|
|
||||||
field(:non_followers, :boolean, default: true)
|
|
||||||
field(:privacy_option, :boolean, default: false)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def changeset(schema, params) do
|
def changeset(schema, params) do
|
||||||
schema
|
schema
|
||||||
|> cast(prepare_attrs(params), [
|
|> cast(prepare_attrs(params), [
|
||||||
:followers,
|
:block_from_strangers,
|
||||||
:follows,
|
:hide_notification_contents
|
||||||
:non_follows,
|
|
||||||
:non_followers,
|
|
||||||
:privacy_option
|
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ defmodule Pleroma.User.Query do
|
||||||
external: boolean(),
|
external: boolean(),
|
||||||
active: boolean(),
|
active: boolean(),
|
||||||
deactivated: boolean(),
|
deactivated: boolean(),
|
||||||
|
need_approval: boolean(),
|
||||||
is_admin: boolean(),
|
is_admin: boolean(),
|
||||||
is_moderator: boolean(),
|
is_moderator: boolean(),
|
||||||
super_users: boolean(),
|
super_users: boolean(),
|
||||||
|
@ -146,6 +147,10 @@ defp compose_query({:deactivated, true}, query) do
|
||||||
|> where([u], not is_nil(u.nickname))
|
|> where([u], not is_nil(u.nickname))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp compose_query({:need_approval, _}, query) do
|
||||||
|
where(query, [u], u.approval_pending)
|
||||||
|
end
|
||||||
|
|
||||||
defp compose_query({:followers, %User{id: id}}, query) do
|
defp compose_query({:followers, %User{id: id}}, query) do
|
||||||
query
|
query
|
||||||
|> where([u], u.id != ^id)
|
|> where([u], u.id != ^id)
|
||||||
|
|
62
lib/pleroma/user/welcome_email.ex
Normal file
62
lib/pleroma/user/welcome_email.ex
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.User.WelcomeEmail do
|
||||||
|
@moduledoc """
|
||||||
|
The module represents the functions to send welcome email.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Emails
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
import Pleroma.Config.Helpers, only: [instance_name: 0]
|
||||||
|
|
||||||
|
@spec enabled?() :: boolean()
|
||||||
|
def enabled?, do: Config.get([:welcome, :email, :enabled], false)
|
||||||
|
|
||||||
|
@spec send_email(User.t()) :: {:ok, Oban.Job.t()}
|
||||||
|
def send_email(%User{} = user) do
|
||||||
|
user
|
||||||
|
|> Emails.UserEmail.welcome(email_options(user))
|
||||||
|
|> Emails.Mailer.deliver_async()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp email_options(user) do
|
||||||
|
bindings = [user: user, instance_name: instance_name()]
|
||||||
|
|
||||||
|
%{}
|
||||||
|
|> add_sender(Config.get([:welcome, :email, :sender], nil))
|
||||||
|
|> add_option(:subject, bindings)
|
||||||
|
|> add_option(:html, bindings)
|
||||||
|
|> add_option(:text, bindings)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_option(opts, option, bindings) do
|
||||||
|
[:welcome, :email, option]
|
||||||
|
|> Config.get(nil)
|
||||||
|
|> eval_string(bindings)
|
||||||
|
|> merge_options(opts, option)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_sender(opts, {_name, _email} = sender) do
|
||||||
|
merge_options(sender, opts, :sender)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_sender(opts, sender) when is_binary(sender) do
|
||||||
|
add_sender(opts, {instance_name(), sender})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_sender(opts, _), do: opts
|
||||||
|
|
||||||
|
defp merge_options(nil, options, _option), do: options
|
||||||
|
|
||||||
|
defp merge_options(value, options, option) do
|
||||||
|
Map.merge(options, %{option => value})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp eval_string(nil, _), do: nil
|
||||||
|
defp eval_string("", _), do: nil
|
||||||
|
defp eval_string(str, bindings), do: EEx.eval_string(str, bindings)
|
||||||
|
end
|
|
@ -3,32 +3,45 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.User.WelcomeMessage do
|
defmodule Pleroma.User.WelcomeMessage do
|
||||||
|
alias Pleroma.Config
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
def post_welcome_message_to_user(user) do
|
@spec enabled?() :: boolean()
|
||||||
with %User{} = sender_user <- welcome_user(),
|
def enabled?, do: Config.get([:welcome, :direct_message, :enabled], false)
|
||||||
message when is_binary(message) <- welcome_message() do
|
|
||||||
CommonAPI.post(sender_user, %{
|
@spec post_message(User.t()) :: {:ok, Pleroma.Activity.t() | nil}
|
||||||
visibility: "direct",
|
def post_message(user) do
|
||||||
status: "@#{user.nickname}\n#{message}"
|
[:welcome, :direct_message, :sender_nickname]
|
||||||
})
|
|> Config.get(nil)
|
||||||
else
|
|> fetch_sender()
|
||||||
_ -> {:ok, nil}
|
|> do_post(user, welcome_message())
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp welcome_user do
|
defp do_post(%User{} = sender, %User{nickname: nickname}, message)
|
||||||
with nickname when is_binary(nickname) <-
|
when is_binary(message) do
|
||||||
Pleroma.Config.get([:instance, :welcome_user_nickname]),
|
CommonAPI.post(
|
||||||
%User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
sender,
|
||||||
|
%{
|
||||||
|
visibility: "direct",
|
||||||
|
status: "@#{nickname}\n#{message}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_post(_sender, _recipient, _message), do: {:ok, nil}
|
||||||
|
|
||||||
|
defp fetch_sender(nickname) when is_binary(nickname) do
|
||||||
|
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
user
|
user
|
||||||
else
|
else
|
||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp fetch_sender(_), do: nil
|
||||||
|
|
||||||
defp welcome_message do
|
defp welcome_message do
|
||||||
Pleroma.Config.get([:instance, :welcome_message])
|
Config.get([:welcome, :direct_message, :message], nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1359,6 +1359,10 @@ def fetch_and_prepare_user_from_ap_id(ap_id) do
|
||||||
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||||
{:error, e}
|
{:error, e}
|
||||||
|
|
||||||
|
{:error, {:reject, reason} = e} ->
|
||||||
|
Logger.info("Rejected user #{ap_id}: #{inspect(reason)}")
|
||||||
|
{:error, e}
|
||||||
|
|
||||||
{:error, e} ->
|
{:error, e} ->
|
||||||
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||||
{:error, e}
|
{:error, e}
|
||||||
|
|
|
@ -60,7 +60,7 @@ def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
|
||||||
if score < 0.8 do
|
if score < 0.8 do
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
else
|
else
|
||||||
{:reject, nil}
|
{:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -39,14 +39,13 @@ def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
|
|
||||||
{:old_user, false} ->
|
{:old_user, false} ->
|
||||||
{:reject, nil}
|
{:reject, "[AntiLinkSpamPolicy] User has no posts nor followers"}
|
||||||
|
|
||||||
{:error, _} ->
|
{:error, _} ->
|
||||||
{:reject, nil}
|
{:reject, "[AntiLinkSpamPolicy] Failed to get or fetch user by ap_id"}
|
||||||
|
|
||||||
e ->
|
e ->
|
||||||
Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}")
|
{:reject, "[AntiLinkSpamPolicy] Unhandled error #{inspect(e)}"}
|
||||||
{:reject, nil}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,8 @@ def filter_by_summary(
|
||||||
|
|
||||||
def filter_by_summary(_in_reply_to, child), do: child
|
def filter_by_summary(_in_reply_to, child), do: child
|
||||||
|
|
||||||
def filter(%{"type" => "Create", "object" => child_object} = object) do
|
def filter(%{"type" => "Create", "object" => child_object} = object)
|
||||||
|
when is_map(child_object) do
|
||||||
child =
|
child =
|
||||||
child_object["inReplyTo"]
|
child_object["inReplyTo"]
|
||||||
|> Object.normalize(child_object["inReplyTo"])
|
|> Object.normalize(child_object["inReplyTo"])
|
||||||
|
|
|
@ -43,7 +43,7 @@ defp delist_message(message, _threshold), do: {:ok, message}
|
||||||
defp reject_message(message, threshold) when threshold > 0 do
|
defp reject_message(message, threshold) when threshold > 0 do
|
||||||
with {_, recipients} <- get_recipient_count(message) do
|
with {_, recipients} <- get_recipient_count(message) do
|
||||||
if recipients > threshold do
|
if recipients > threshold do
|
||||||
{:reject, nil}
|
{:reject, "[HellthreadPolicy] #{recipients} recipients is over the limit of #{threshold}"}
|
||||||
else
|
else
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
end
|
end
|
||||||
|
@ -87,7 +87,7 @@ def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = message
|
||||||
{:ok, message} <- delist_message(message, delist_threshold) do
|
{:ok, message} <- delist_message(message, delist_threshold) do
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
else
|
else
|
||||||
_e -> {:reject, nil}
|
e -> e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} =
|
||||||
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
|
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
|
||||||
string_matches?(content, pattern) or string_matches?(summary, pattern)
|
string_matches?(content, pattern) or string_matches?(summary, pattern)
|
||||||
end) do
|
end) do
|
||||||
{:reject, nil}
|
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
|
||||||
else
|
else
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
end
|
end
|
||||||
|
@ -89,8 +89,9 @@ def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message
|
||||||
{:ok, message} <- check_replace(message) do
|
{:ok, message} <- check_replace(message) do
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
else
|
else
|
||||||
_e ->
|
{:reject, nil} -> {:reject, "[KeywordPolicy] "}
|
||||||
{:reject, nil}
|
{:reject, _} = e -> e
|
||||||
|
_e -> {:reject, "[KeywordPolicy] "}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,9 @@ def filter(%{"type" => "Create"} = message) do
|
||||||
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])
|
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])
|
||||||
recipients = (message["to"] || []) ++ (message["cc"] || [])
|
recipients = (message["to"] || []) ++ (message["cc"] || [])
|
||||||
|
|
||||||
if Enum.any?(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
|
if rejected_mention =
|
||||||
{:reject, nil}
|
Enum.find(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
|
||||||
|
{:reject, "[MentionPolicy] Rejected for mention of #{rejected_mention}"}
|
||||||
else
|
else
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,7 +28,7 @@ defp check_date(%{"object" => %{"published" => published}} = message) do
|
||||||
|
|
||||||
defp check_reject(message, actions) do
|
defp check_reject(message, actions) do
|
||||||
if :reject in actions do
|
if :reject in actions do
|
||||||
{:reject, nil}
|
{:reject, "[ObjectAgePolicy]"}
|
||||||
else
|
else
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
end
|
end
|
||||||
|
@ -47,9 +47,8 @@ defp check_delist(message, actions) do
|
||||||
|
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
else
|
else
|
||||||
# Unhandleable error: somebody is messing around, just drop the message.
|
|
||||||
_e ->
|
_e ->
|
||||||
{:reject, nil}
|
{:reject, "[ObjectAgePolicy] Unhandled error"}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
|
@ -69,9 +68,8 @@ defp check_strip_followers(message, actions) do
|
||||||
|
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
else
|
else
|
||||||
# Unhandleable error: somebody is messing around, just drop the message.
|
|
||||||
_e ->
|
_e ->
|
||||||
{:reject, nil}
|
{:reject, "[ObjectAgePolicy] Unhandled error"}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
|
|
|
@ -38,7 +38,7 @@ def filter(%{"type" => "Create"} = object) do
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
{:reject, nil}
|
{:reject, "[RejectNonPublic] visibility: #{visibility}"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ defp check_accept(%{host: actor_host} = _actor_info, object) do
|
||||||
accepts == [] -> {:ok, object}
|
accepts == [] -> {:ok, object}
|
||||||
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
|
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
|
||||||
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
|
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
|
||||||
true -> {:reject, nil}
|
true -> {:reject, "[SimplePolicy] host not in accept list"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ defp check_reject(%{host: actor_host} = _actor_info, object) do
|
||||||
|> MRF.subdomains_regex()
|
|> MRF.subdomains_regex()
|
||||||
|
|
||||||
if MRF.subdomain_match?(rejects, actor_host) do
|
if MRF.subdomain_match?(rejects, actor_host) do
|
||||||
{:reject, nil}
|
{:reject, "[SimplePolicy] host in reject list"}
|
||||||
else
|
else
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
end
|
end
|
||||||
|
@ -114,7 +114,7 @@ defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"}
|
||||||
|> MRF.subdomains_regex()
|
|> MRF.subdomains_regex()
|
||||||
|
|
||||||
if MRF.subdomain_match?(report_removal, actor_host) do
|
if MRF.subdomain_match?(report_removal, actor_host) do
|
||||||
{:reject, nil}
|
{:reject, "[SimplePolicy] host in report_removal list"}
|
||||||
else
|
else
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
end
|
end
|
||||||
|
@ -159,7 +159,7 @@ def filter(%{"type" => "Delete", "actor" => actor} = object) do
|
||||||
|> MRF.subdomains_regex()
|
|> MRF.subdomains_regex()
|
||||||
|
|
||||||
if MRF.subdomain_match?(reject_deletes, actor_host) do
|
if MRF.subdomain_match?(reject_deletes, actor_host) do
|
||||||
{:reject, nil}
|
{:reject, "[SimplePolicy] host in reject_deletes list"}
|
||||||
else
|
else
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
end
|
end
|
||||||
|
@ -177,7 +177,9 @@ def filter(%{"actor" => actor} = object) do
|
||||||
{:ok, object} <- check_report_removal(actor_info, object) do
|
{:ok, object} <- check_report_removal(actor_info, object) do
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
else
|
else
|
||||||
_e -> {:reject, nil}
|
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||||
|
{:reject, _} = e -> e
|
||||||
|
_ -> {:reject, "[SimplePolicy]"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -191,7 +193,9 @@ def filter(%{"id" => actor, "type" => obj_type} = object)
|
||||||
{:ok, object} <- check_banner_removal(actor_info, object) do
|
{:ok, object} <- check_banner_removal(actor_info, object) do
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
else
|
else
|
||||||
_e -> {:reject, nil}
|
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||||
|
{:reject, _} = e -> e
|
||||||
|
_ -> {:reject, "[SimplePolicy]"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -134,12 +134,13 @@ defp process_tag(
|
||||||
if user.local == true do
|
if user.local == true do
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
else
|
else
|
||||||
{:reject, nil}
|
{:reject,
|
||||||
|
"[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-remote-subscription"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}),
|
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow", "actor" => actor}),
|
||||||
do: {:reject, nil}
|
do: {:reject, "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-any-subscription"}
|
||||||
|
|
||||||
defp process_tag(_, message), do: {:ok, message}
|
defp process_tag(_, message), do: {:ok, message}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ defp filter_by_list(%{"actor" => actor} = object, allow_list) do
|
||||||
if actor in allow_list do
|
if actor in allow_list do
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
else
|
else
|
||||||
{:reject, nil}
|
{:reject, "[UserAllowListPolicy] #{actor} not in the list"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11,22 +11,26 @@ def filter(%{"type" => "Undo", "object" => child_message} = message) do
|
||||||
with {:ok, _} <- filter(child_message) do
|
with {:ok, _} <- filter(child_message) do
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
else
|
else
|
||||||
{:reject, nil} ->
|
{:reject, _} = e -> e
|
||||||
{:reject, nil}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter(%{"type" => message_type} = message) do
|
def filter(%{"type" => message_type} = message) do
|
||||||
with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]),
|
with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]),
|
||||||
rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]),
|
rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]),
|
||||||
true <-
|
{_, true} <-
|
||||||
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type),
|
{:accepted,
|
||||||
false <-
|
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type)},
|
||||||
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type),
|
{_, false} <-
|
||||||
|
{:rejected,
|
||||||
|
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type)},
|
||||||
{:ok, _} <- filter(message["object"]) do
|
{:ok, _} <- filter(message["object"]) do
|
||||||
{:ok, message}
|
{:ok, message}
|
||||||
else
|
else
|
||||||
_ -> {:reject, nil}
|
{:reject, _} = e -> e
|
||||||
|
{:accepted, _} -> {:reject, "[VocabularyPolicy] #{message_type} not in accept list"}
|
||||||
|
{:rejected, _} -> {:reject, "[VocabularyPolicy] #{message_type} in reject list"}
|
||||||
|
_ -> {:reject, "[VocabularyPolicy]"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
||||||
the system.
|
the system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias Pleroma.Activity
|
||||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
@ -74,6 +75,12 @@ def validate(%{"type" => "Undo"} = object, meta) do
|
||||||
|> UndoValidator.cast_and_validate()
|
|> UndoValidator.cast_and_validate()
|
||||||
|> Ecto.Changeset.apply_action(:insert) do
|
|> Ecto.Changeset.apply_action(:insert) do
|
||||||
object = stringify_keys(object)
|
object = stringify_keys(object)
|
||||||
|
undone_object = Activity.get_by_ap_id(object["object"])
|
||||||
|
|
||||||
|
meta =
|
||||||
|
meta
|
||||||
|
|> Keyword.put(:object_data, undone_object.data)
|
||||||
|
|
||||||
{:ok, object, meta}
|
{:ok, object, meta}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -52,6 +52,13 @@ defp maybe_federate(%Activity{} = activity, meta) do
|
||||||
do_not_federate = meta[:do_not_federate] || !Config.get([:instance, :federating])
|
do_not_federate = meta[:do_not_federate] || !Config.get([:instance, :federating])
|
||||||
|
|
||||||
if !do_not_federate && local do
|
if !do_not_federate && local do
|
||||||
|
activity =
|
||||||
|
if object = Keyword.get(meta, :object_data) do
|
||||||
|
%{activity | data: Map.put(activity.data, "object", object)}
|
||||||
|
else
|
||||||
|
activity
|
||||||
|
end
|
||||||
|
|
||||||
Federator.publish(activity)
|
Federator.publish(activity)
|
||||||
{:ok, :federated}
|
{:ok, :federated}
|
||||||
else
|
else
|
||||||
|
|
|
@ -49,7 +49,8 @@ def is_representable?(%Activity{} = activity) do
|
||||||
"""
|
"""
|
||||||
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
|
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
|
||||||
Logger.debug("Federating #{id} to #{inbox}")
|
Logger.debug("Federating #{id} to #{inbox}")
|
||||||
%{host: host, path: path} = URI.parse(inbox)
|
|
||||||
|
uri = URI.parse(inbox)
|
||||||
|
|
||||||
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
|
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
|
||||||
|
|
||||||
|
@ -57,8 +58,8 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
|
||||||
|
|
||||||
signature =
|
signature =
|
||||||
Pleroma.Signature.sign(actor, %{
|
Pleroma.Signature.sign(actor, %{
|
||||||
"(request-target)": "post #{path}",
|
"(request-target)": "post #{uri.path}",
|
||||||
host: host,
|
host: signature_host(uri),
|
||||||
"content-length": byte_size(json),
|
"content-length": byte_size(json),
|
||||||
digest: digest,
|
digest: digest,
|
||||||
date: date
|
date: date
|
||||||
|
@ -76,8 +77,9 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
|
||||||
{"digest", digest}
|
{"digest", digest}
|
||||||
]
|
]
|
||||||
) do
|
) do
|
||||||
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
|
if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
|
||||||
do: Instances.set_reachable(inbox)
|
Instances.set_reachable(inbox)
|
||||||
|
end
|
||||||
|
|
||||||
result
|
result
|
||||||
else
|
else
|
||||||
|
@ -96,6 +98,14 @@ def publish_one(%{actor_id: actor_id} = params) do
|
||||||
|> publish_one()
|
|> publish_one()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
|
||||||
|
if port == URI.default_port(scheme) do
|
||||||
|
host
|
||||||
|
else
|
||||||
|
"#{host}:#{port}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp should_federate?(inbox, public) do
|
defp should_federate?(inbox, public) do
|
||||||
if public do
|
if public do
|
||||||
true
|
true
|
||||||
|
|
|
@ -183,7 +183,7 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
|
||||||
|> Map.drop(["conversation"])
|
|> Map.drop(["conversation"])
|
||||||
else
|
else
|
||||||
e ->
|
e ->
|
||||||
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
|
Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
|
||||||
object
|
object
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|
|
@ -719,15 +719,18 @@ defp build_flag_object(act) when is_map(act) or is_binary(act) do
|
||||||
|
|
||||||
case Activity.get_by_ap_id_with_object(id) do
|
case Activity.get_by_ap_id_with_object(id) do
|
||||||
%Activity{} = activity ->
|
%Activity{} = activity ->
|
||||||
|
activity_actor = User.get_by_ap_id(activity.object.data["actor"])
|
||||||
|
|
||||||
%{
|
%{
|
||||||
"type" => "Note",
|
"type" => "Note",
|
||||||
"id" => activity.data["id"],
|
"id" => activity.data["id"],
|
||||||
"content" => activity.object.data["content"],
|
"content" => activity.object.data["content"],
|
||||||
"published" => activity.object.data["published"],
|
"published" => activity.object.data["published"],
|
||||||
"actor" =>
|
"actor" =>
|
||||||
AccountView.render("show.json", %{
|
AccountView.render(
|
||||||
user: User.get_by_ap_id(activity.object.data["actor"])
|
"show.json",
|
||||||
})
|
%{user: activity_actor, skip_visibility_check: true}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
|
|
|
@ -44,6 +44,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||||
:user_toggle_activation,
|
:user_toggle_activation,
|
||||||
:user_activate,
|
:user_activate,
|
||||||
:user_deactivate,
|
:user_deactivate,
|
||||||
|
:user_approve,
|
||||||
:tag_users,
|
:tag_users,
|
||||||
:untag_users,
|
:untag_users,
|
||||||
:right_add,
|
:right_add,
|
||||||
|
@ -303,6 +304,21 @@ def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nickname
|
||||||
|> render("index.json", %{users: Keyword.values(updated_users)})
|
|> render("index.json", %{users: Keyword.values(updated_users)})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user_approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||||
|
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||||
|
{:ok, updated_users} = User.approve(users)
|
||||||
|
|
||||||
|
ModerationLog.insert_log(%{
|
||||||
|
actor: admin,
|
||||||
|
subject: users,
|
||||||
|
action: "approve"
|
||||||
|
})
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_view(AccountView)
|
||||||
|
|> render("index.json", %{users: updated_users})
|
||||||
|
end
|
||||||
|
|
||||||
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||||
with {:ok, _} <- User.tag(nicknames, tags) do
|
with {:ok, _} <- User.tag(nicknames, tags) do
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{
|
||||||
|
@ -345,12 +361,16 @@ def list_users(conn, params) do
|
||||||
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
|
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
|
||||||
json(
|
json(
|
||||||
conn,
|
conn,
|
||||||
AccountView.render("index.json", users: users, count: count, page_size: page_size)
|
AccountView.render("index.json",
|
||||||
|
users: users,
|
||||||
|
count: count,
|
||||||
|
page_size: page_size
|
||||||
|
)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@filters ~w(local external active deactivated is_admin is_moderator)
|
@filters ~w(local external active deactivated need_approval is_admin is_moderator)
|
||||||
|
|
||||||
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
|
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
|
||||||
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
|
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
|
||||||
|
@ -616,29 +636,24 @@ def reload_emoji(conn, _params) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||||
|
|
||||||
User.toggle_confirmation(users)
|
User.toggle_confirmation(users)
|
||||||
|
|
||||||
ModerationLog.insert_log(%{
|
ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"})
|
||||||
actor: admin,
|
|
||||||
subject: users,
|
|
||||||
action: "confirm_email"
|
|
||||||
})
|
|
||||||
|
|
||||||
json(conn, "")
|
json(conn, "")
|
||||||
end
|
end
|
||||||
|
|
||||||
def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
users =
|
||||||
|
Enum.map(nicknames, fn nickname ->
|
||||||
|
nickname
|
||||||
|
|> User.get_cached_by_nickname()
|
||||||
|
|> User.send_confirmation_email()
|
||||||
|
end)
|
||||||
|
|
||||||
User.try_send_confirmation_email(users)
|
ModerationLog.insert_log(%{actor: admin, subject: users, action: "resend_confirmation_email"})
|
||||||
|
|
||||||
ModerationLog.insert_log(%{
|
|
||||||
actor: admin,
|
|
||||||
subject: users,
|
|
||||||
action: "resend_confirmation_email"
|
|
||||||
})
|
|
||||||
|
|
||||||
json(conn, "")
|
json(conn, "")
|
||||||
end
|
end
|
||||||
|
|
|
@ -77,7 +77,9 @@ def render("show.json", %{user: user}) do
|
||||||
"roles" => User.roles(user),
|
"roles" => User.roles(user),
|
||||||
"tags" => user.tags || [],
|
"tags" => user.tags || [],
|
||||||
"confirmation_pending" => user.confirmation_pending,
|
"confirmation_pending" => user.confirmation_pending,
|
||||||
"url" => user.uri || user.ap_id
|
"approval_pending" => user.approval_pending,
|
||||||
|
"url" => user.uri || user.ap_id,
|
||||||
|
"registration_reason" => user.registration_reason
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -105,7 +107,7 @@ def render("create-error.json", %{changeset: %Ecto.Changeset{changes: changes, e
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_account_views(%User{} = user) do
|
def merge_account_views(%User{} = user) do
|
||||||
MastodonAPI.AccountView.render("show.json", %{user: user})
|
MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|
||||||
|> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
|
|> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -159,6 +159,7 @@ def followers_operation do
|
||||||
"Accounts which follow the given account, if network is not hidden by the account owner.",
|
"Accounts which follow the given account, if network is not hidden by the account owner.",
|
||||||
parameters: [
|
parameters: [
|
||||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||||
|
Operation.parameter(:id, :query, :string, "ID of the resource owner"),
|
||||||
with_relationships_param() | pagination_params()
|
with_relationships_param() | pagination_params()
|
||||||
],
|
],
|
||||||
responses: %{
|
responses: %{
|
||||||
|
@ -177,6 +178,7 @@ def following_operation do
|
||||||
"Accounts which the given account is following, if network is not hidden by the account owner.",
|
"Accounts which the given account is following, if network is not hidden by the account owner.",
|
||||||
parameters: [
|
parameters: [
|
||||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||||
|
Operation.parameter(:id, :query, :string, "ID of the resource owner"),
|
||||||
with_relationships_param() | pagination_params()
|
with_relationships_param() | pagination_params()
|
||||||
],
|
],
|
||||||
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
|
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
|
||||||
|
|
|
@ -300,11 +300,11 @@ def chat_messages_response do
|
||||||
"content" => "Check this out :firefox:",
|
"content" => "Check this out :firefox:",
|
||||||
"id" => "13",
|
"id" => "13",
|
||||||
"chat_id" => "1",
|
"chat_id" => "1",
|
||||||
"actor_id" => "someflakeid",
|
"account_id" => "someflakeid",
|
||||||
"unread" => false
|
"unread" => false
|
||||||
},
|
},
|
||||||
%{
|
%{
|
||||||
"actor_id" => "someflakeid",
|
"account_id" => "someflakeid",
|
||||||
"content" => "Whats' up?",
|
"content" => "Whats' up?",
|
||||||
"id" => "12",
|
"id" => "12",
|
||||||
"chat_id" => "1",
|
"chat_id" => "1",
|
||||||
|
|
|
@ -31,6 +31,7 @@ def index_operation do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Supporting domain query parameter is deprecated in Mastodon API
|
||||||
def create_operation do
|
def create_operation do
|
||||||
%Operation{
|
%Operation{
|
||||||
tags: ["domain_blocks"],
|
tags: ["domain_blocks"],
|
||||||
|
@ -45,11 +46,13 @@ def create_operation do
|
||||||
""",
|
""",
|
||||||
operationId: "DomainBlockController.create",
|
operationId: "DomainBlockController.create",
|
||||||
requestBody: domain_block_request(),
|
requestBody: domain_block_request(),
|
||||||
|
parameters: [Operation.parameter(:domain, :query, %Schema{type: :string}, "Domain name")],
|
||||||
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
||||||
responses: %{200 => empty_object_response()}
|
responses: %{200 => empty_object_response()}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Supporting domain query parameter is deprecated in Mastodon API
|
||||||
def delete_operation do
|
def delete_operation do
|
||||||
%Operation{
|
%Operation{
|
||||||
tags: ["domain_blocks"],
|
tags: ["domain_blocks"],
|
||||||
|
@ -57,6 +60,7 @@ def delete_operation do
|
||||||
description: "Remove a domain block, if it exists in the user's array of blocked domains.",
|
description: "Remove a domain block, if it exists in the user's array of blocked domains.",
|
||||||
operationId: "DomainBlockController.delete",
|
operationId: "DomainBlockController.delete",
|
||||||
requestBody: domain_block_request(),
|
requestBody: domain_block_request(),
|
||||||
|
parameters: [Operation.parameter(:domain, :query, %Schema{type: :string}, "Domain name")],
|
||||||
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
security: [%{"oAuth" => ["follow", "write:blocks"]}],
|
||||||
responses: %{
|
responses: %{
|
||||||
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
|
||||||
|
@ -71,10 +75,9 @@ defp domain_block_request do
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
domain: %Schema{type: :string}
|
domain: %Schema{type: :string}
|
||||||
},
|
}
|
||||||
required: [:domain]
|
|
||||||
},
|
},
|
||||||
required: true,
|
required: false,
|
||||||
example: %{
|
example: %{
|
||||||
"domain" => "facebook.com"
|
"domain" => "facebook.com"
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,11 +90,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
||||||
notification_settings: %Schema{
|
notification_settings: %Schema{
|
||||||
type: :object,
|
type: :object,
|
||||||
properties: %{
|
properties: %{
|
||||||
followers: %Schema{type: :boolean},
|
block_from_strangers: %Schema{type: :boolean},
|
||||||
follows: %Schema{type: :boolean},
|
hide_notification_contents: %Schema{type: :boolean}
|
||||||
non_followers: %Schema{type: :boolean},
|
|
||||||
non_follows: %Schema{type: :boolean},
|
|
||||||
privacy_option: %Schema{type: :boolean}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
relationship: AccountRelationship,
|
relationship: AccountRelationship,
|
||||||
|
@ -182,11 +179,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
||||||
"unread_conversation_count" => 0,
|
"unread_conversation_count" => 0,
|
||||||
"tags" => [],
|
"tags" => [],
|
||||||
"notification_settings" => %{
|
"notification_settings" => %{
|
||||||
"followers" => true,
|
"block_from_strangers" => false,
|
||||||
"follows" => true,
|
"hide_notification_contents" => false
|
||||||
"non_followers" => true,
|
|
||||||
"non_follows" => true,
|
|
||||||
"privacy_option" => false
|
|
||||||
},
|
},
|
||||||
"relationship" => %{
|
"relationship" => %{
|
||||||
"blocked_by" => false,
|
"blocked_by" => false,
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
|
|
||||||
defmodule Pleroma.Web.ChatChannel do
|
defmodule Pleroma.Web.ChatChannel do
|
||||||
use Phoenix.Channel
|
use Phoenix.Channel
|
||||||
|
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ChatChannel.ChatChannelState
|
alias Pleroma.Web.ChatChannel.ChatChannelState
|
||||||
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
|
|
||||||
def join("chat:public", _message, socket) do
|
def join("chat:public", _message, socket) do
|
||||||
send(self(), :after_join)
|
send(self(), :after_join)
|
||||||
|
@ -22,9 +24,9 @@ def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}}
|
||||||
|
|
||||||
if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do
|
if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do
|
||||||
author = User.get_cached_by_nickname(user_name)
|
author = User.get_cached_by_nickname(user_name)
|
||||||
author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author)
|
author_json = AccountView.render("show.json", user: author, skip_visibility_check: true)
|
||||||
|
|
||||||
message = ChatChannelState.add_message(%{text: text, author: author})
|
message = ChatChannelState.add_message(%{text: text, author: author_json})
|
||||||
|
|
||||||
broadcast!(socket, "new_msg", message)
|
broadcast!(socket, "new_msg", message)
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,6 +28,17 @@ defmodule Pleroma.Web.Endpoint do
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Careful! No `only` restriction here, as we don't know what frontends contain.
|
||||||
|
plug(Pleroma.Plugs.FrontendStatic,
|
||||||
|
at: "/",
|
||||||
|
frontend_type: :primary,
|
||||||
|
gzip: true,
|
||||||
|
cache_control_for_etags: @static_cache_control,
|
||||||
|
headers: %{
|
||||||
|
"cache-control" => @static_cache_control
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Serve at "/" the static files from "priv/static" directory.
|
# Serve at "/" the static files from "priv/static" directory.
|
||||||
#
|
#
|
||||||
# You should set gzip to true if you are running phoenix.digest
|
# You should set gzip to true if you are running phoenix.digest
|
||||||
|
|
|
@ -47,7 +47,7 @@ def feed(conn, %{"nickname" => nickname} = params) do
|
||||||
"atom"
|
"atom"
|
||||||
end
|
end
|
||||||
|
|
||||||
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
|
with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
|
||||||
activities =
|
activities =
|
||||||
%{
|
%{
|
||||||
type: ["Create"],
|
type: ["Create"],
|
||||||
|
@ -71,6 +71,7 @@ def errors(conn, {:error, :not_found}) do
|
||||||
render_error(conn, :not_found, "Not found")
|
render_error(conn, :not_found, "Not found")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def errors(conn, {:fetch_user, %User{local: false}}), do: errors(conn, {:error, :not_found})
|
||||||
def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
|
def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
|
||||||
|
|
||||||
def errors(conn, _) do
|
def errors(conn, _) do
|
||||||
|
|
|
@ -32,9 +32,19 @@ def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn,
|
||||||
json(conn, %{})
|
json(conn, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
|
||||||
|
User.block_domain(blocker, domain)
|
||||||
|
json(conn, %{})
|
||||||
|
end
|
||||||
|
|
||||||
@doc "DELETE /api/v1/domain_blocks"
|
@doc "DELETE /api/v1/domain_blocks"
|
||||||
def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
|
def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
|
||||||
User.unblock_domain(blocker, domain)
|
User.unblock_domain(blocker, domain)
|
||||||
json(conn, %{})
|
json(conn, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
|
||||||
|
User.unblock_domain(blocker, domain)
|
||||||
|
json(conn, %{})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -93,7 +93,6 @@ defp resource_search(_, "accounts", query, options) do
|
||||||
AccountView.render("index.json",
|
AccountView.render("index.json",
|
||||||
users: accounts,
|
users: accounts,
|
||||||
for: options[:for_user],
|
for: options[:for_user],
|
||||||
as: :user,
|
|
||||||
embed_relationships: options[:embed_relationships]
|
embed_relationships: options[:embed_relationships]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -172,6 +172,11 @@ def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn,
|
||||||
with_direct_conversation_id: true
|
with_direct_conversation_id: true
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
{:error, {:reject, message}} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unprocessable_entity)
|
||||||
|
|> json(%{error: message})
|
||||||
|
|
||||||
{:error, message} ->
|
{:error, message} ->
|
||||||
conn
|
conn
|
||||||
|> put_status(:unprocessable_entity)
|
|> put_status(:unprocessable_entity)
|
||||||
|
|
|
@ -27,21 +27,40 @@ def render("index.json", %{users: users} = opts) do
|
||||||
UserRelationship.view_relationships_option(reading_user, users)
|
UserRelationship.view_relationships_option(reading_user, users)
|
||||||
end
|
end
|
||||||
|
|
||||||
opts = Map.put(opts, :relationships, relationships_opt)
|
opts =
|
||||||
|
opts
|
||||||
|
|> Map.merge(%{relationships: relationships_opt, as: :user})
|
||||||
|
|> Map.delete(:users)
|
||||||
|
|
||||||
users
|
users
|
||||||
|> render_many(AccountView, "show.json", opts)
|
|> render_many(AccountView, "show.json", opts)
|
||||||
|> Enum.filter(&Enum.any?/1)
|
|> Enum.filter(&Enum.any?/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("show.json", %{user: user} = opts) do
|
@doc """
|
||||||
if User.visible_for(user, opts[:for]) == :visible do
|
Renders specified user account.
|
||||||
|
:skip_visibility_check option skips visibility check and renders any user (local or remote)
|
||||||
|
regardless of [:pleroma, :restrict_unauthenticated] setting.
|
||||||
|
:for option specifies the requester and can be a User record or nil.
|
||||||
|
Only use `user: user, for: user` when `user` is the actual requester of own profile.
|
||||||
|
"""
|
||||||
|
def render("show.json", %{user: _user, skip_visibility_check: true} = opts) do
|
||||||
|
do_render("show.json", opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render("show.json", %{user: user, for: for_user_or_nil} = opts) do
|
||||||
|
if User.visible_for(user, for_user_or_nil) == :visible do
|
||||||
do_render("show.json", opts)
|
do_render("show.json", opts)
|
||||||
else
|
else
|
||||||
%{}
|
%{}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render("show.json", _) do
|
||||||
|
raise "In order to prevent account accessibility issues, " <>
|
||||||
|
":skip_visibility_check or :for option is required."
|
||||||
|
end
|
||||||
|
|
||||||
def render("mention.json", %{user: user}) do
|
def render("mention.json", %{user: user}) do
|
||||||
%{
|
%{
|
||||||
id: to_string(user.id),
|
id: to_string(user.id),
|
||||||
|
|
|
@ -38,7 +38,7 @@ def render("participation.json", %{participation: participation, for: user}) do
|
||||||
|
|
||||||
%{
|
%{
|
||||||
id: participation.id |> to_string(),
|
id: participation.id |> to_string(),
|
||||||
accounts: render(AccountView, "index.json", users: users, as: :user),
|
accounts: render(AccountView, "index.json", users: users, for: user),
|
||||||
unread: !participation.read,
|
unread: !participation.read,
|
||||||
last_status:
|
last_status:
|
||||||
render(StatusView, "show.json",
|
render(StatusView, "show.json",
|
||||||
|
|
|
@ -26,6 +26,7 @@ def render("show.json", _) do
|
||||||
thumbnail: Keyword.get(instance, :instance_thumbnail),
|
thumbnail: Keyword.get(instance, :instance_thumbnail),
|
||||||
languages: ["en"],
|
languages: ["en"],
|
||||||
registrations: Keyword.get(instance, :registrations_open),
|
registrations: Keyword.get(instance, :registrations_open),
|
||||||
|
approval_required: Keyword.get(instance, :account_approval_required),
|
||||||
# Extra (not present in Mastodon):
|
# Extra (not present in Mastodon):
|
||||||
max_toot_chars: Keyword.get(instance, :limit),
|
max_toot_chars: Keyword.get(instance, :limit),
|
||||||
poll_limits: Keyword.get(instance, :poll_limits),
|
poll_limits: Keyword.get(instance, :poll_limits),
|
||||||
|
@ -41,7 +42,8 @@ def render("show.json", _) do
|
||||||
account_activation_required: Keyword.get(instance, :account_activation_required),
|
account_activation_required: Keyword.get(instance, :account_activation_required),
|
||||||
features: features(),
|
features: features(),
|
||||||
federation: federation(),
|
federation: federation(),
|
||||||
fields_limits: fields_limits()
|
fields_limits: fields_limits(),
|
||||||
|
post_formats: Config.get([:instance, :allowed_post_formats])
|
||||||
},
|
},
|
||||||
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
|
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -297,13 +297,17 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
||||||
|
|
||||||
emoji_reactions =
|
emoji_reactions =
|
||||||
with %{data: %{"reactions" => emoji_reactions}} <- object do
|
with %{data: %{"reactions" => emoji_reactions}} <- object do
|
||||||
Enum.map(emoji_reactions, fn [emoji, users] ->
|
Enum.map(emoji_reactions, fn
|
||||||
%{
|
[emoji, users] when is_list(users) ->
|
||||||
name: emoji,
|
build_emoji_map(emoji, users, opts[:for])
|
||||||
count: length(users),
|
|
||||||
me: !!(opts[:for] && opts[:for].ap_id in users)
|
{emoji, users} when is_list(users) ->
|
||||||
}
|
build_emoji_map(emoji, users, opts[:for])
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
end)
|
end)
|
||||||
|
|> Enum.reject(&is_nil/1)
|
||||||
else
|
else
|
||||||
_ -> []
|
_ -> []
|
||||||
end
|
end
|
||||||
|
@ -545,4 +549,12 @@ defp present?(_), do: true
|
||||||
|
|
||||||
defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
|
defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
|
||||||
do: id in pinned_activities
|
do: id in pinned_activities
|
||||||
|
|
||||||
|
defp build_emoji_map(emoji, users, current_user) do
|
||||||
|
%{
|
||||||
|
name: emoji,
|
||||||
|
count: length(users),
|
||||||
|
me: !!(current_user && current_user.ap_id in users)
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -337,6 +337,16 @@ defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :confirm
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
|
||||||
|
render_error(
|
||||||
|
conn,
|
||||||
|
:forbidden,
|
||||||
|
"Your account is awaiting approval.",
|
||||||
|
%{},
|
||||||
|
"awaiting_approval"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
|
defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
|
||||||
render_invalid_credentials_error(conn)
|
render_invalid_credentials_error(conn)
|
||||||
end
|
end
|
||||||
|
|
|
@ -89,11 +89,11 @@ def post_chat_message(
|
||||||
cm_ref <- MessageReference.for_chat_and_object(chat, message) do
|
cm_ref <- MessageReference.for_chat_and_object(chat, message) do
|
||||||
conn
|
conn
|
||||||
|> put_view(MessageReferenceView)
|
|> put_view(MessageReferenceView)
|
||||||
|> render("show.json", for: user, chat_message_reference: cm_ref)
|
|> render("show.json", chat_message_reference: cm_ref)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
|
def mark_message_as_read(%{assigns: %{user: %{id: user_id}}} = conn, %{
|
||||||
id: chat_id,
|
id: chat_id,
|
||||||
message_id: message_id
|
message_id: message_id
|
||||||
}) do
|
}) do
|
||||||
|
@ -104,12 +104,15 @@ def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
|
||||||
{:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do
|
{:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do
|
||||||
conn
|
conn
|
||||||
|> put_view(MessageReferenceView)
|
|> put_view(MessageReferenceView)
|
||||||
|> render("show.json", for: user, chat_message_reference: cm_ref)
|
|> render("show.json", chat_message_reference: cm_ref)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_as_read(
|
def mark_as_read(
|
||||||
%{body_params: %{last_read_id: last_read_id}, assigns: %{user: %{id: user_id}}} = conn,
|
%{
|
||||||
|
body_params: %{last_read_id: last_read_id},
|
||||||
|
assigns: %{user: %{id: user_id}}
|
||||||
|
} = conn,
|
||||||
%{id: id}
|
%{id: id}
|
||||||
) do
|
) do
|
||||||
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
|
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
|
||||||
|
@ -121,7 +124,7 @@ def mark_as_read(
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do
|
def messages(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id} = params) do
|
||||||
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do
|
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do
|
||||||
cm_refs =
|
cm_refs =
|
||||||
chat
|
chat
|
||||||
|
@ -130,7 +133,7 @@ def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = para
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_view(MessageReferenceView)
|
|> put_view(MessageReferenceView)
|
||||||
|> render("index.json", for: user, chat_message_references: cm_refs)
|
|> render("index.json", chat_message_references: cm_refs)
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
conn
|
conn
|
||||||
|
|
|
@ -21,8 +21,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
|
@skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
|
||||||
plug(:skip_plug, @skip_plugs when action in [:archive, :show, :list])
|
plug(:skip_plug, @skip_plugs when action in [:index, :show, :archive])
|
||||||
|
|
||||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,11 @@ defmodule Pleroma.Web.PleromaAPI.ChatView do
|
||||||
def render("show.json", %{chat: %Chat{} = chat} = opts) do
|
def render("show.json", %{chat: %Chat{} = chat} = opts) do
|
||||||
recipient = User.get_cached_by_ap_id(chat.recipient)
|
recipient = User.get_cached_by_ap_id(chat.recipient)
|
||||||
last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat)
|
last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat)
|
||||||
|
account_view_opts = account_view_opts(opts, recipient)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
id: chat.id |> to_string(),
|
id: chat.id |> to_string(),
|
||||||
account: AccountView.render("show.json", Map.put(opts, :user, recipient)),
|
account: AccountView.render("show.json", account_view_opts),
|
||||||
unread: MessageReference.unread_count_for_chat(chat),
|
unread: MessageReference.unread_count_for_chat(chat),
|
||||||
last_message:
|
last_message:
|
||||||
last_message &&
|
last_message &&
|
||||||
|
@ -27,7 +28,17 @@ def render("show.json", %{chat: %Chat{} = chat} = opts) do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("index.json", %{chats: chats}) do
|
def render("index.json", %{chats: chats} = opts) do
|
||||||
render_many(chats, __MODULE__, "show.json")
|
render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp account_view_opts(opts, recipient) do
|
||||||
|
account_view_opts = Map.put(opts, :user, recipient)
|
||||||
|
|
||||||
|
if Map.has_key?(account_view_opts, :for) do
|
||||||
|
account_view_opts
|
||||||
|
else
|
||||||
|
Map.put(account_view_opts, :skip_visibility_check, true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,7 +17,7 @@ def render("show.json", %{emoji_reaction: [emoji, user_ap_ids], user: user}) do
|
||||||
%{
|
%{
|
||||||
name: emoji,
|
name: emoji,
|
||||||
count: length(users),
|
count: length(users),
|
||||||
accounts: render(AccountView, "index.json", users: users, for: user, as: :user),
|
accounts: render(AccountView, "index.json", users: users, for: user),
|
||||||
me: !!(user && user.ap_id in user_ap_ids)
|
me: !!(user && user.ap_id in user_ap_ids)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -104,7 +104,7 @@ def build_content(notification, actor, object, mastodon_type \\ nil)
|
||||||
|
|
||||||
def build_content(
|
def build_content(
|
||||||
%{
|
%{
|
||||||
user: %{notification_settings: %{privacy_option: true}}
|
user: %{notification_settings: %{hide_notification_contents: true}}
|
||||||
} = notification,
|
} = notification,
|
||||||
_actor,
|
_actor,
|
||||||
_object,
|
_object,
|
||||||
|
|
|
@ -11,10 +11,10 @@ defmodule Pleroma.Web.RichMedia.Helpers do
|
||||||
|
|
||||||
@spec validate_page_url(URI.t() | binary()) :: :ok | :error
|
@spec validate_page_url(URI.t() | binary()) :: :ok | :error
|
||||||
defp validate_page_url(page_url) when is_binary(page_url) do
|
defp validate_page_url(page_url) when is_binary(page_url) do
|
||||||
validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld]
|
validate_tld = Pleroma.Config.get([Pleroma.Formatter, :validate_tld])
|
||||||
|
|
||||||
page_url
|
page_url
|
||||||
|> AutoLinker.Parser.url?(scheme: true, validate_tld: validate_tld)
|
|> Linkify.Parser.url?(validate_tld: validate_tld)
|
||||||
|> parse_uri(page_url)
|
|> parse_uri(page_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -138,6 +138,7 @@ defmodule Pleroma.Web.Router do
|
||||||
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
|
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
|
||||||
patch("/users/activate", AdminAPIController, :user_activate)
|
patch("/users/activate", AdminAPIController, :user_activate)
|
||||||
patch("/users/deactivate", AdminAPIController, :user_deactivate)
|
patch("/users/deactivate", AdminAPIController, :user_deactivate)
|
||||||
|
patch("/users/approve", AdminAPIController, :user_approve)
|
||||||
put("/users/tag", AdminAPIController, :tag_users)
|
put("/users/tag", AdminAPIController, :tag_users)
|
||||||
delete("/users/tag", AdminAPIController, :untag_users)
|
delete("/users/tag", AdminAPIController, :untag_users)
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= label f, :code, "Recovery code" %>
|
<%= label f, :code, "Recovery code" %>
|
||||||
<%= text_input f, :code %>
|
<%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, spellcheck: false] %>
|
||||||
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
||||||
<%= hidden_input f, :state, value: @state %>
|
<%= hidden_input f, :state, value: @state %>
|
||||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
<%= form_for @conn, mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= label f, :code, "Authentication code" %>
|
<%= label f, :code, "Authentication code" %>
|
||||||
<%= text_input f, :code %>
|
<%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
|
||||||
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
||||||
<%= hidden_input f, :state, value: @state %>
|
<%= hidden_input f, :state, value: @state %>
|
||||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue