Merge remote-tracking branch 'remotes/origin/develop' into 2168-media-preview-proxy
# Conflicts: # config/config.exs # lib/pleroma/web/media_proxy/media_proxy.ex # mix.lock # test/web/media_proxy/media_proxy_test.exs
This commit is contained in:
commit
bdf57b8ef4
|
@ -5,6 +5,7 @@
|
|||
/*.ez
|
||||
/test/uploads
|
||||
/.elixir_ls
|
||||
/test/fixtures/DSCN0010_tmp.jpg
|
||||
/test/fixtures/test_tmp.txt
|
||||
/test/fixtures/image_tmp.jpg
|
||||
/test/tmp/
|
||||
|
|
|
@ -60,26 +60,25 @@ unit-testing:
|
|||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
script:
|
||||
- apt-get update && apt-get install -y libimage-exiftool-perl
|
||||
- mix deps.get
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- mix coveralls --preload-modules
|
||||
|
||||
# Removed to fix CI issue. In this early state it wasn't adding much value anyway.
|
||||
# TODO Fix and reinstate federated testing
|
||||
# federated-testing:
|
||||
# stage: test
|
||||
# cache: *testing_cache_policy
|
||||
# services:
|
||||
# - name: minibikini/postgres-with-rum:12
|
||||
# alias: postgres
|
||||
# command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
# script:
|
||||
# - mix deps.get
|
||||
# - mix ecto.create
|
||||
# - mix ecto.migrate
|
||||
# - epmd -daemon
|
||||
# - mix test --trace --only federated
|
||||
federated-testing:
|
||||
stage: test
|
||||
cache: *testing_cache_policy
|
||||
services:
|
||||
- name: minibikini/postgres-with-rum:12
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
script:
|
||||
- mix deps.get
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- epmd -daemon
|
||||
- mix test --trace --only federated
|
||||
|
||||
unit-testing-rum:
|
||||
stage: test
|
||||
|
@ -93,6 +92,7 @@ unit-testing-rum:
|
|||
<<: *global_variables
|
||||
RUM_ENABLED: "true"
|
||||
script:
|
||||
- apt-get update && apt-get install -y libimage-exiftool-perl
|
||||
- mix deps.get
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
|
|
|
@ -8,9 +8,7 @@
|
|||
|
||||
### Environment
|
||||
|
||||
* Installation type:
|
||||
- [ ] OTP
|
||||
- [ ] From source
|
||||
* Installation type (OTP or From Source):
|
||||
* 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):
|
||||
* Operating system:
|
||||
|
|
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -12,12 +12,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- MFR policy to set global expiration for all local Create activities
|
||||
- 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: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated.
|
||||
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
|
||||
- **Breaking:** Pleroma API: The routes to update avatar, banner and background have been removed.
|
||||
- **Breaking:** Image description length is limited now.
|
||||
- **Breaking:** Emoji API: changed methods and renamed routes.
|
||||
- MastodonAPI: Allow removal of avatar, banner and background.
|
||||
- Streaming: Repeats of a user's posts will no longer be pushed to the user's stream.
|
||||
- Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance
|
||||
- Mastodon API: On deletion, returns the original post text.
|
||||
- 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
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
@ -33,6 +45,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
### Added
|
||||
|
||||
- Chats: Added `accepts_chat_messages` field to user, exposed in APIs and federation.
|
||||
- Chats: Added support for federated chats. For details, see the docs.
|
||||
- ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
|
||||
- Instance: Add `background_image` to configuration and `/api/v1/instance`
|
||||
|
@ -49,6 +62,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Added `:reject_deletes` group to SimplePolicy
|
||||
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
|
||||
- Support pagination in emoji packs API (for packs and for files in pack)
|
||||
- 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.
|
||||
|
||||
<details>
|
||||
<summary>API Changes</summary>
|
||||
|
@ -56,8 +71,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Mastodon API: Extended `/api/v1/instance`.
|
||||
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
|
||||
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
|
||||
- Mastodon API: Add support for filtering replies in public and home timelines
|
||||
- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials`
|
||||
- Mastodon API: Add support for filtering replies in public and home timelines.
|
||||
- Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials`.
|
||||
- Mastodon API: Support irreversible property for filters.
|
||||
- Mastodon API: Add pleroma.favicon field to accounts.
|
||||
- Admin API: endpoints for create/update/delete OAuth Apps.
|
||||
- Admin API: endpoint for status view.
|
||||
- OTP: Add command to reload emoji packs
|
||||
|
@ -71,6 +88,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Resolving Peertube accounts with Webfinger
|
||||
- `blob:` urls not being allowed by connect-src CSP
|
||||
- Mastodon API: fix `GET /api/v1/notifications` not returning the full result set
|
||||
- Rich Media Previews for Twitter links
|
||||
- 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 edge case where MediaProxy truncates media, usually caused when Caddy is serving content for the other Federated instance.
|
||||
|
||||
## [Unreleased (patch)]
|
||||
|
||||
|
@ -212,7 +233,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Mastodon API: `pleroma.thread_muted` to the Status entity
|
||||
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
|
||||
- Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
|
||||
- Mastodon API: Add `pleroma.unread_count` to the Marker entity
|
||||
- Admin API: Render whole status in grouped reports
|
||||
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
|
||||
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
|
||||
|
|
|
@ -33,7 +33,7 @@ ARG DATA=/var/lib/pleroma
|
|||
|
||||
RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\
|
||||
apk update &&\
|
||||
apk add imagemagick ncurses postgresql-client &&\
|
||||
apk add exiftool imagemagick ncurses postgresql-client &&\
|
||||
adduser --system --shell /bin/false --home ${HOME} pleroma &&\
|
||||
mkdir -p ${DATA}/uploads &&\
|
||||
mkdir -p ${DATA}/static &&\
|
||||
|
|
|
@ -24,6 +24,7 @@ defmodule Pleroma.LoadTesting.Activities do
|
|||
@visibility ~w(public private direct unlisted)
|
||||
@types [
|
||||
:simple,
|
||||
:simple_filtered,
|
||||
:emoji,
|
||||
:mentions,
|
||||
:hell_thread,
|
||||
|
@ -242,6 +243,15 @@ defp insert_activity(:simple, visibility, group, users, _opts) do
|
|||
insert_local_activity(visibility, group, users, "Simple status")
|
||||
end
|
||||
|
||||
defp insert_activity(:simple_filtered, visibility, group, users, _opts)
|
||||
when group in @remote_groups do
|
||||
insert_remote_activity(visibility, group, users, "Remote status which must be filtered")
|
||||
end
|
||||
|
||||
defp insert_activity(:simple_filtered, visibility, group, users, _opts) do
|
||||
insert_local_activity(visibility, group, users, "Simple status which must be filtered")
|
||||
end
|
||||
|
||||
defp insert_activity(:emoji, visibility, group, users, _opts)
|
||||
when group in @remote_groups do
|
||||
insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:")
|
||||
|
|
|
@ -32,10 +32,22 @@ defp fetch_user(user) do
|
|||
)
|
||||
end
|
||||
|
||||
defp create_filter(user) do
|
||||
Pleroma.Filter.create(%Pleroma.Filter{
|
||||
user_id: user.id,
|
||||
phrase: "must be filtered",
|
||||
hide: true
|
||||
})
|
||||
end
|
||||
|
||||
defp delete_filter(filter), do: Repo.delete(filter)
|
||||
|
||||
defp fetch_timelines(user) do
|
||||
fetch_home_timeline(user)
|
||||
fetch_home_timeline_with_filter(user)
|
||||
fetch_direct_timeline(user)
|
||||
fetch_public_timeline(user)
|
||||
fetch_public_timeline_with_filter(user)
|
||||
fetch_public_timeline(user, :with_blocks)
|
||||
fetch_public_timeline(user, :local)
|
||||
fetch_public_timeline(user, :tag)
|
||||
|
@ -61,7 +73,7 @@ defp opts_for_home_timeline(user) do
|
|||
}
|
||||
end
|
||||
|
||||
defp fetch_home_timeline(user) do
|
||||
defp fetch_home_timeline(user, title_end \\ "") do
|
||||
opts = opts_for_home_timeline(user)
|
||||
|
||||
recipients = [user.ap_id | User.following(user)]
|
||||
|
@ -84,9 +96,11 @@ defp fetch_home_timeline(user) do
|
|||
|> Enum.reverse()
|
||||
|> List.last()
|
||||
|
||||
title = "home timeline " <> title_end
|
||||
|
||||
Benchee.run(
|
||||
%{
|
||||
"home timeline" => fn opts -> ActivityPub.fetch_activities(recipients, opts) end
|
||||
title => fn opts -> ActivityPub.fetch_activities(recipients, opts) end
|
||||
},
|
||||
inputs: %{
|
||||
"1 page" => opts,
|
||||
|
@ -108,6 +122,14 @@ defp fetch_home_timeline(user) do
|
|||
)
|
||||
end
|
||||
|
||||
defp fetch_home_timeline_with_filter(user) do
|
||||
{:ok, filter} = create_filter(user)
|
||||
|
||||
fetch_home_timeline(user, "with filters")
|
||||
|
||||
delete_filter(filter)
|
||||
end
|
||||
|
||||
defp opts_for_direct_timeline(user) do
|
||||
%{
|
||||
visibility: "direct",
|
||||
|
@ -210,6 +232,14 @@ defp fetch_public_timeline(user) do
|
|||
fetch_public_timeline(opts, "public timeline")
|
||||
end
|
||||
|
||||
defp fetch_public_timeline_with_filter(user) do
|
||||
{:ok, filter} = create_filter(user)
|
||||
opts = opts_for_public_timeline(user)
|
||||
|
||||
fetch_public_timeline(opts, "public timeline with filters")
|
||||
delete_filter(filter)
|
||||
end
|
||||
|
||||
defp fetch_public_timeline(user, :local) do
|
||||
opts = opts_for_public_timeline(user, :local)
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
"dat",
|
||||
"dweb",
|
||||
"gopher",
|
||||
"hyper",
|
||||
"ipfs",
|
||||
"ipns",
|
||||
"irc",
|
||||
|
@ -171,7 +172,7 @@
|
|||
"application/ld+json" => ["activity+json"]
|
||||
}
|
||||
|
||||
config :tesla, adapter: Tesla.Adapter.Hackney
|
||||
config :tesla, adapter: Tesla.Adapter.Gun
|
||||
|
||||
# Configures http settings, upstream proxy etc.
|
||||
config :pleroma, :http,
|
||||
|
@ -188,6 +189,7 @@
|
|||
background_image: "/images/city.jpg",
|
||||
instance_thumbnail: "/instance/thumbnail.jpeg",
|
||||
limit: 5_000,
|
||||
description_limit: 5_000,
|
||||
chat_limit: 5_000,
|
||||
remote_limit: 100_000,
|
||||
upload_limit: 16_000_000,
|
||||
|
@ -448,8 +450,7 @@
|
|||
|
||||
config :pleroma, Pleroma.Web.Preload,
|
||||
providers: [
|
||||
Pleroma.Web.Preload.Providers.Instance,
|
||||
Pleroma.Web.Preload.Providers.StatusNet
|
||||
Pleroma.Web.Preload.Providers.Instance
|
||||
]
|
||||
|
||||
config :pleroma, :http_security,
|
||||
|
@ -509,8 +510,7 @@
|
|||
|
||||
config :pleroma, Oban,
|
||||
repo: Pleroma.Repo,
|
||||
verbose: false,
|
||||
prune: {:maxlen, 1500},
|
||||
log: false,
|
||||
queues: [
|
||||
activity_expiration: 10,
|
||||
federator_incoming: 50,
|
||||
|
@ -524,6 +524,7 @@
|
|||
attachments_cleanup: 5,
|
||||
new_users_digest: 1
|
||||
],
|
||||
plugins: [Oban.Plugins.Pruner],
|
||||
crontab: [
|
||||
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
|
||||
{"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
|
||||
|
@ -659,32 +660,30 @@
|
|||
prepare: :unnamed
|
||||
|
||||
config :pleroma, :connections_pool,
|
||||
checkin_timeout: 250,
|
||||
reclaim_multiplier: 0.1,
|
||||
connection_acquisition_wait: 250,
|
||||
connection_acquisition_retries: 5,
|
||||
max_connections: 250,
|
||||
retry: 1,
|
||||
retry_timeout: 1000,
|
||||
max_idle_time: 30_000,
|
||||
retry: 0,
|
||||
await_up_timeout: 5_000
|
||||
|
||||
config :pleroma, :pools,
|
||||
federation: [
|
||||
size: 50,
|
||||
max_overflow: 10,
|
||||
timeout: 150_000
|
||||
max_waiting: 10
|
||||
],
|
||||
media: [
|
||||
size: 50,
|
||||
max_overflow: 10,
|
||||
timeout: 150_000
|
||||
max_waiting: 10
|
||||
],
|
||||
upload: [
|
||||
size: 25,
|
||||
max_overflow: 5,
|
||||
timeout: 300_000
|
||||
max_waiting: 5
|
||||
],
|
||||
default: [
|
||||
size: 10,
|
||||
max_overflow: 2,
|
||||
timeout: 10_000
|
||||
max_waiting: 2
|
||||
]
|
||||
|
||||
config :pleroma, :hackney_pools,
|
||||
|
@ -717,6 +716,8 @@
|
|||
|
||||
config :ex_aws, http_client: Pleroma.HTTP.ExAws
|
||||
|
||||
config :pleroma, :instances_favicons, enabled: false
|
||||
|
||||
config :pleroma, :exexec,
|
||||
root_mode: false,
|
||||
options: %{}
|
||||
|
|
|
@ -23,18 +23,14 @@
|
|||
key: :uploader,
|
||||
type: :module,
|
||||
description: "Module which will be used for uploads",
|
||||
suggestions: [Pleroma.Uploaders.Local, Pleroma.Uploaders.S3]
|
||||
suggestions: {:list_behaviour_implementations, Pleroma.Uploaders.Uploader}
|
||||
},
|
||||
%{
|
||||
key: :filters,
|
||||
type: {:list, :module},
|
||||
description:
|
||||
"List of filter modules for uploads. Module names are shortened (removed leading `Pleroma.Upload.Filter.` part), but on adding custom module you need to use full name.",
|
||||
suggestions:
|
||||
Generator.list_modules_in_dir(
|
||||
"lib/pleroma/upload/filter",
|
||||
"Elixir.Pleroma.Upload.Filter."
|
||||
)
|
||||
suggestions: {:list_behaviour_implementations, Pleroma.Upload.Filter}
|
||||
},
|
||||
%{
|
||||
key: :link_name,
|
||||
|
@ -196,7 +192,9 @@
|
|||
%{
|
||||
key: :args,
|
||||
type: [:string, {:list, :string}, {:list, :tuple}],
|
||||
description: "List of actions for the mogrify command",
|
||||
description:
|
||||
"List of actions for the mogrify command. It's possible to add self-written settings as string. " <>
|
||||
"For example `[\"auto-orient\", \"strip\", {\"resize\", \"3840x1080>\"}]` string will be parsed into list of the settings.",
|
||||
suggestions: [
|
||||
"strip",
|
||||
"auto-orient",
|
||||
|
@ -496,6 +494,7 @@
|
|||
"dat",
|
||||
"dweb",
|
||||
"gopher",
|
||||
"hyper",
|
||||
"ipfs",
|
||||
"ipns",
|
||||
"irc",
|
||||
|
@ -697,8 +696,9 @@
|
|||
key: :public,
|
||||
type: :boolean,
|
||||
description:
|
||||
"Makes the client API in authentificated mode-only except for user-profiles." <>
|
||||
" Useful for disabling the Local Timeline and The Whole Known Network."
|
||||
"Makes the client API in authenticated mode-only except for user-profiles." <>
|
||||
" Useful for disabling the Local Timeline and The Whole Known Network. " <>
|
||||
" Note: when setting to `false`, please also check `:restrict_unauthenticated` setting."
|
||||
},
|
||||
%{
|
||||
key: :quarantined_instances,
|
||||
|
@ -1067,6 +1067,7 @@
|
|||
},
|
||||
%{
|
||||
key: :webhook_url,
|
||||
label: "Webhook URL",
|
||||
type: :string,
|
||||
description: "Configure the Slack incoming webhook",
|
||||
suggestions: ["https://hooks.slack.com/services/YOUR-KEY-HERE"]
|
||||
|
@ -1387,12 +1388,47 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf,
|
||||
tab: :mrf,
|
||||
label: "MRF",
|
||||
type: :group,
|
||||
description: "General MRF settings",
|
||||
children: [
|
||||
%{
|
||||
key: :policies,
|
||||
type: [:module, {:list, :module}],
|
||||
description:
|
||||
"A list of MRF policies enabled. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
|
||||
suggestions: {:list_behaviour_implementations, Pleroma.Web.ActivityPub.MRF}
|
||||
},
|
||||
%{
|
||||
key: :transparency,
|
||||
label: "MRF transparency",
|
||||
type: :boolean,
|
||||
description:
|
||||
"Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
|
||||
},
|
||||
%{
|
||||
key: :transparency_exclusions,
|
||||
label: "MRF transparency exclusions",
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.",
|
||||
suggestions: [
|
||||
"exclusion.com"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf_simple,
|
||||
tab: :mrf,
|
||||
label: "MRF Simple",
|
||||
type: :group,
|
||||
description: "Message Rewrite Facility",
|
||||
description: "Simple ingress policies",
|
||||
children: [
|
||||
%{
|
||||
key: :media_removal,
|
||||
|
@ -1411,7 +1447,7 @@
|
|||
key: :federated_timeline_removal,
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"List of instances to remove from Federated (aka The Whole Known Network) Timeline",
|
||||
"List of instances to remove from the Federated (aka The Whole Known Network) Timeline",
|
||||
suggestions: ["example.com", "*.example.com"]
|
||||
},
|
||||
%{
|
||||
|
@ -1455,14 +1491,15 @@
|
|||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf_activity_expiration,
|
||||
tab: :mrf,
|
||||
label: "MRF Activity Expiration Policy",
|
||||
type: :group,
|
||||
description: "Adds expiration to all local Create Note activities",
|
||||
description: "Adds automatic expiration to all local activities",
|
||||
children: [
|
||||
%{
|
||||
key: :days,
|
||||
type: :integer,
|
||||
description: "Default global expiration time for all local Create activities (in days)",
|
||||
description: "Default global expiration time for all local activities (in days)",
|
||||
suggestions: [90, 365]
|
||||
}
|
||||
]
|
||||
|
@ -1470,6 +1507,7 @@
|
|||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf_subchain,
|
||||
tab: :mrf,
|
||||
label: "MRF Subchain",
|
||||
type: :group,
|
||||
description:
|
||||
|
@ -1478,7 +1516,7 @@
|
|||
children: [
|
||||
%{
|
||||
key: :match_actor,
|
||||
type: :map,
|
||||
type: {:map, {:list, :string}},
|
||||
description: "Matches a series of regular expressions against the actor field",
|
||||
suggestions: [
|
||||
%{
|
||||
|
@ -1491,8 +1529,8 @@
|
|||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf_rejectnonpublic,
|
||||
description:
|
||||
"MRF RejectNonPublic settings. RejectNonPublic drops posts with non-public visibility settings.",
|
||||
tab: :mrf,
|
||||
description: "RejectNonPublic drops posts with non-public visibility settings.",
|
||||
label: "MRF Reject Non Public",
|
||||
type: :group,
|
||||
children: [
|
||||
|
@ -1512,16 +1550,17 @@
|
|||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf_hellthread,
|
||||
tab: :mrf,
|
||||
label: "MRF Hellthread",
|
||||
type: :group,
|
||||
description: "Block messages with too much mentions",
|
||||
description: "Block messages with excessive user mentions",
|
||||
children: [
|
||||
%{
|
||||
key: :delist_threshold,
|
||||
type: :integer,
|
||||
description:
|
||||
"Number of mentioned users after which the message gets delisted (the message can still be seen, " <>
|
||||
" but it will not show up in public timelines and mentioned users won't get notifications about it). Set to 0 to disable.",
|
||||
"Number of mentioned users after which the message gets removed from timelines and" <>
|
||||
"disables notifications. Set to 0 to disable.",
|
||||
suggestions: [10]
|
||||
},
|
||||
%{
|
||||
|
@ -1536,27 +1575,28 @@
|
|||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf_keyword,
|
||||
tab: :mrf,
|
||||
label: "MRF Keyword",
|
||||
type: :group,
|
||||
description: "Reject or Word-Replace messages with a keyword or regex",
|
||||
children: [
|
||||
%{
|
||||
key: :reject,
|
||||
type: [:string, :regex],
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"A list of patterns which result in message being rejected. Each pattern can be a string or a regular expression.",
|
||||
suggestions: ["foo", ~r/foo/iu]
|
||||
},
|
||||
%{
|
||||
key: :federated_timeline_removal,
|
||||
type: [:string, :regex],
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"A list of patterns which result in message being removed from federated timelines (a.k.a unlisted). Each pattern can be a string or a regular expression.",
|
||||
suggestions: ["foo", ~r/foo/iu]
|
||||
},
|
||||
%{
|
||||
key: :replace,
|
||||
type: [{:tuple, :string, :string}, {:tuple, :regex, :string}],
|
||||
type: {:list, :tuple},
|
||||
description:
|
||||
"A list of tuples containing {pattern, replacement}. Each pattern can be a string or a regular expression.",
|
||||
suggestions: [{"foo", "bar"}, {~r/foo/iu, "bar"}]
|
||||
|
@ -1566,9 +1606,10 @@
|
|||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf_mention,
|
||||
tab: :mrf,
|
||||
label: "MRF Mention",
|
||||
type: :group,
|
||||
description: "Block messages which mention a user",
|
||||
description: "Block messages which mention a specific user",
|
||||
children: [
|
||||
%{
|
||||
key: :actors,
|
||||
|
@ -1581,6 +1622,7 @@
|
|||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf_vocabulary,
|
||||
tab: :mrf,
|
||||
label: "MRF Vocabulary",
|
||||
type: :group,
|
||||
description: "Filter messages which belong to certain activity vocabularies",
|
||||
|
@ -1726,8 +1768,8 @@
|
|||
%{
|
||||
key: :whitelist,
|
||||
type: {:list, :string},
|
||||
description: "List of domains to bypass the mediaproxy",
|
||||
suggestions: ["example.com"]
|
||||
description: "List of hosts with scheme to bypass the mediaproxy",
|
||||
suggestions: ["http://example.com"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1744,15 +1786,20 @@
|
|||
},
|
||||
%{
|
||||
key: :headers,
|
||||
type: {:list, :tuple},
|
||||
description: "HTTP headers of request.",
|
||||
type: {:keyword, :string},
|
||||
description: "HTTP headers of request",
|
||||
suggestions: [{"x-refresh", 1}]
|
||||
},
|
||||
%{
|
||||
key: :options,
|
||||
type: :keyword,
|
||||
description: "Request options.",
|
||||
suggestions: [params: %{ts: "xxx"}]
|
||||
description: "Request options",
|
||||
children: [
|
||||
%{
|
||||
key: :params,
|
||||
type: {:map, :string}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1961,13 +2008,15 @@
|
|||
label: "Pleroma Admin Token",
|
||||
type: :group,
|
||||
description:
|
||||
"Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the `admin_token` parameter",
|
||||
"Allows setting a token that can be used to authenticate requests with admin privileges without a normal user account token. Append the `admin_token` parameter to requests to utilize it. (Please reconsider using HTTP Basic Auth or OAuth-based authentication if possible)",
|
||||
children: [
|
||||
%{
|
||||
key: :admin_token,
|
||||
type: :string,
|
||||
description: "Admin token",
|
||||
suggestions: ["We recommend a secure random string or UUID"]
|
||||
suggestions: [
|
||||
"Please use a high entropy string or UUID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1985,18 +2034,11 @@
|
|||
""",
|
||||
children: [
|
||||
%{
|
||||
key: :verbose,
|
||||
key: :log,
|
||||
type: {:dropdown, :atom},
|
||||
description: "Logs verbose mode",
|
||||
suggestions: [false, :error, :warn, :info, :debug]
|
||||
},
|
||||
%{
|
||||
key: :prune,
|
||||
type: [:atom, :tuple],
|
||||
description:
|
||||
"Non-retryable jobs [pruning settings](https://github.com/sorentwo/oban#pruning)",
|
||||
suggestions: [:disabled, {:maxlen, 1500}, {:maxage, 60 * 60}]
|
||||
},
|
||||
%{
|
||||
key: :queues,
|
||||
type: {:keyword, :integer},
|
||||
|
@ -2475,7 +2517,7 @@
|
|||
%{
|
||||
key: :styling,
|
||||
type: :map,
|
||||
description: "a map with color settings for email templates.",
|
||||
description: "A map with color settings for email templates.",
|
||||
suggestions: [
|
||||
%{
|
||||
link_color: "#d8a070",
|
||||
|
@ -2530,8 +2572,7 @@
|
|||
%{
|
||||
key: :enabled,
|
||||
type: :boolean,
|
||||
description: "Enables new users admin digest email when `true`",
|
||||
suggestions: [false]
|
||||
description: "Enables new users admin digest email when `true`"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2581,7 +2622,7 @@
|
|||
},
|
||||
%{
|
||||
key: :groups,
|
||||
type: {:keyword, :string, {:list, :string}},
|
||||
type: {:keyword, {:list, :string}},
|
||||
description:
|
||||
"Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the group name" <>
|
||||
" and the value is the location or array of locations. * can be used as a wildcard.",
|
||||
|
@ -2861,6 +2902,7 @@
|
|||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
tab: :mrf,
|
||||
key: :mrf_normalize_markup,
|
||||
label: "MRF Normalize Markup",
|
||||
description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
|
||||
|
@ -3057,8 +3099,10 @@
|
|||
group: :pleroma,
|
||||
key: :mrf_object_age,
|
||||
label: "MRF Object Age",
|
||||
tab: :mrf,
|
||||
type: :group,
|
||||
description: "Rejects or delists posts based on their age when received.",
|
||||
description:
|
||||
"Rejects or delists posts based on their timestamp deviance from your server's clock.",
|
||||
children: [
|
||||
%{
|
||||
key: :threshold,
|
||||
|
@ -3071,7 +3115,7 @@
|
|||
type: {:list, :atom},
|
||||
description:
|
||||
"A list of actions to apply to the post. `:delist` removes the post from public timelines; " <>
|
||||
"`:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines; " <>
|
||||
"`:strip_followers` removes followers from the ActivityPub recipient list ensuring they won't be delivered to home timelines; " <>
|
||||
"`:reject` rejects the message entirely",
|
||||
suggestions: [:delist, :strip_followers, :reject]
|
||||
}
|
||||
|
@ -3117,36 +3161,37 @@
|
|||
description: "Advanced settings for `gun` connections pool",
|
||||
children: [
|
||||
%{
|
||||
key: :checkin_timeout,
|
||||
key: :connection_acquisition_wait,
|
||||
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]
|
||||
},
|
||||
%{
|
||||
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,
|
||||
type: :integer,
|
||||
description: "Maximum number of connections in the pool. Default: 250 connections.",
|
||||
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,
|
||||
type: :integer,
|
||||
description: "Timeout while `gun` will wait until connection is up. Default: 5000ms.",
|
||||
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]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -3155,108 +3200,29 @@
|
|||
key: :pools,
|
||||
type: :group,
|
||||
description: "Advanced settings for `gun` workers pools",
|
||||
children: [
|
||||
%{
|
||||
key: :federation,
|
||||
type: :keyword,
|
||||
description: "Settings for federation 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: :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]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
children:
|
||||
Enum.map([:federation, :media, :upload, :default], fn pool_name ->
|
||||
%{
|
||||
key: pool_name,
|
||||
type: :keyword,
|
||||
description: "Settings for #{pool_name} pool.",
|
||||
children: [
|
||||
%{
|
||||
key: :size,
|
||||
type: :integer,
|
||||
description: "Maximum number of concurrent requests in the pool.",
|
||||
suggestions: [50]
|
||||
},
|
||||
%{
|
||||
key: :max_waiting,
|
||||
type: :integer,
|
||||
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]
|
||||
}
|
||||
]
|
||||
}
|
||||
end)
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
|
@ -3392,44 +3358,46 @@
|
|||
key: :strict,
|
||||
type: :boolean,
|
||||
description:
|
||||
"Enables strict input validation (useful in development, not recommended in production)",
|
||||
suggestions: [false]
|
||||
"Enables strict input validation (useful in development, not recommended in production)"
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: :mrf,
|
||||
key: :instances_favicons,
|
||||
type: :group,
|
||||
description: "General MRF settings",
|
||||
description: "Control favicons for instances",
|
||||
children: [
|
||||
%{
|
||||
key: :policies,
|
||||
type: [:module, {:list, :module}],
|
||||
description:
|
||||
"A list of MRF policies enabled. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
|
||||
suggestions:
|
||||
Generator.list_modules_in_dir(
|
||||
"lib/pleroma/web/activity_pub/mrf",
|
||||
"Elixir.Pleroma.Web.ActivityPub.MRF."
|
||||
)
|
||||
},
|
||||
%{
|
||||
key: :transparency,
|
||||
label: "MRF transparency",
|
||||
key: :enabled,
|
||||
type: :boolean,
|
||||
description:
|
||||
"Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
|
||||
description: "Allow/disallow displaying and getting instances favicons"
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :ex_aws,
|
||||
key: :s3,
|
||||
type: :group,
|
||||
descriptions: "S3 service related settings",
|
||||
children: [
|
||||
%{
|
||||
key: :access_key_id,
|
||||
type: :string,
|
||||
description: "S3 access key ID",
|
||||
suggestions: ["AKIAQ8UKHTGIYN7DMWWJ"]
|
||||
},
|
||||
%{
|
||||
key: :transparency_exclusions,
|
||||
label: "MRF transparency exclusions",
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.",
|
||||
suggestions: [
|
||||
"exclusion.com"
|
||||
]
|
||||
key: :secret_access_key,
|
||||
type: :string,
|
||||
description: "Secret access key",
|
||||
suggestions: ["JFGt+fgH1UQ7vLUQjpW+WvjTdV/UNzVxcwn7DkaeFKtBS5LvoXvIiME4NQBsT6ZZ"]
|
||||
},
|
||||
%{
|
||||
key: :host,
|
||||
type: :string,
|
||||
description: "S3 host",
|
||||
suggestions: ["s3.eu-central-1.amazonaws.com"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -79,8 +79,8 @@
|
|||
|
||||
config :pleroma, Oban,
|
||||
queues: false,
|
||||
prune: :disabled,
|
||||
crontab: false
|
||||
crontab: false,
|
||||
plugins: false
|
||||
|
||||
config :pleroma, Pleroma.ScheduledActivity,
|
||||
daily_user_limit: 2,
|
||||
|
@ -111,6 +111,13 @@
|
|||
|
||||
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
|
||||
|
||||
config :pleroma, :instances_favicons, enabled: true
|
||||
|
||||
config :pleroma, Pleroma.Uploaders.S3,
|
||||
bucket: nil,
|
||||
streaming_enabled: true,
|
||||
public_endpoint: nil
|
||||
|
||||
if File.exists?("./config/test.secret.exs") do
|
||||
import_config "test.secret.exs"
|
||||
else
|
||||
|
|
|
@ -64,13 +64,15 @@ Has these additional fields under the `pleroma` object:
|
|||
- `hide_follows`: boolean, true when the user has follow hiding enabled
|
||||
- `hide_followers_count`: boolean, true when the user has follower stat hiding enabled
|
||||
- `hide_follows_count`: boolean, true when the user has follow stat hiding enabled
|
||||
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
|
||||
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
|
||||
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `/api/v1/accounts/verify_credentials` and `/api/v1/accounts/update_credentials`
|
||||
- `chat_token`: The token needed for Pleroma chat. Only returned in `/api/v1/accounts/verify_credentials`
|
||||
- `deactivated`: boolean, true when the user is deactivated
|
||||
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
|
||||
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
|
||||
- `unread_notifications_count`: The count of unread notifications. Only returned to the account owner.
|
||||
- `notification_settings`: object, can be absent. See `/api/pleroma/notification_settings` for the parameters/keys returned.
|
||||
- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user
|
||||
- `favicon`: nullable URL string, Favicon image of the user's instance
|
||||
|
||||
### Source
|
||||
|
||||
|
@ -167,7 +169,7 @@ Returns: array of Status.
|
|||
|
||||
The maximum number of statuses is limited to 100 per request.
|
||||
|
||||
## PATCH `/api/v1/update_credentials`
|
||||
## PATCH `/api/v1/accounts/update_credentials`
|
||||
|
||||
Additional parameters can be added to the JSON body/Form data:
|
||||
|
||||
|
@ -182,9 +184,12 @@ Additional parameters can be added to the JSON body/Form data:
|
|||
- `pleroma_settings_store` - Opaque user settings to be saved on the backend.
|
||||
- `skip_thread_containment` - if true, skip filtering out broken threads
|
||||
- `allow_following_move` - if true, allows automatically follow moved following accounts
|
||||
- `pleroma_background_image` - sets the background image of the user.
|
||||
- `pleroma_background_image` - sets the background image of the user. Can be set to "" (an empty string) to reset.
|
||||
- `discoverable` - if true, discovery of this account in search results and other services is allowed.
|
||||
- `actor_type` - the type of this account.
|
||||
- `accepts_chat_messages` - if false, this account will reject all chat messages.
|
||||
|
||||
All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file.
|
||||
|
||||
### Pleroma Settings Store
|
||||
|
||||
|
@ -192,7 +197,7 @@ Pleroma has mechanism that allows frontends to save blobs of json for each user
|
|||
|
||||
The parameter should have a form of `{frontend_name: {...}}`, with `frontend_name` identifying your type of client, e.g. `pleroma_fe`. It will overwrite everything under this property, but will not overwrite other frontend's settings.
|
||||
|
||||
This information is returned in the `verify_credentials` endpoint.
|
||||
This information is returned in the `/api/v1/accounts/verify_credentials` endpoint.
|
||||
|
||||
## Authentication
|
||||
|
||||
|
@ -220,6 +225,8 @@ Has theses additional parameters (which are the same as in Pleroma-API):
|
|||
`GET /api/v1/instance` has additional fields
|
||||
|
||||
- `max_toot_chars`: The maximum characters per post
|
||||
- `chat_limit`: The maximum characters per chat message
|
||||
- `description_limit`: The maximum characters per image description
|
||||
- `poll_limits`: The limits of polls
|
||||
- `upload_limit`: The maximum upload file size
|
||||
- `avatar_upload_limit`: The same for avatars
|
||||
|
@ -228,6 +235,8 @@ Has theses additional parameters (which are the same as in Pleroma-API):
|
|||
- `background_image`: A background image that frontends can use
|
||||
- `pleroma.metadata.features`: A list of supported features
|
||||
- `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.post_formats`: A list of the allowed post format types
|
||||
- `vapid_public_key`: The public key needed for push messages
|
||||
|
||||
## Markers
|
||||
|
|
|
@ -287,11 +287,8 @@ See [Admin-API](admin_api.md)
|
|||
* Method `PUT`
|
||||
* Authentication: required
|
||||
* Params:
|
||||
* `followers`: BOOLEAN field, receives notifications from followers
|
||||
* `follows`: BOOLEAN field, receives notifications from people the user follows
|
||||
* `remote`: BOOLEAN field, receives notifications from people on remote instances
|
||||
* `local`: BOOLEAN field, receives notifications from people on the local instance
|
||||
* `privacy_option`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification.
|
||||
* `block_from_strangers`: BOOLEAN field, blocks notifications from accounts you do not follow
|
||||
* `hide_notification_contents`: BOOLEAN field. When set to true, it removes the contents of a message from the push notification.
|
||||
* Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}`
|
||||
|
||||
## `/api/pleroma/healthcheck`
|
||||
|
|
|
@ -57,11 +57,11 @@ mix pleroma.user invites
|
|||
|
||||
## Revoke invite
|
||||
```sh tab="OTP"
|
||||
./bin/pleroma_ctl user revoke_invite <token_or_id>
|
||||
./bin/pleroma_ctl user revoke_invite <token>
|
||||
```
|
||||
|
||||
```sh tab="From Source"
|
||||
mix pleroma.user revoke_invite <token_or_id>
|
||||
mix pleroma.user revoke_invite <token>
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Updating your instance
|
||||
|
||||
You should **always check the release notes/changelog** in case there are config deprecations, special update special update steps, etc.
|
||||
You should **always check the [release notes/changelog](https://git.pleroma.social/pleroma/pleroma/-/releases)** in case there are config deprecations, special update steps, etc.
|
||||
|
||||
Besides that, doing the following is generally enough:
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
* `notify_email`: Email used for notifications.
|
||||
* `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``.
|
||||
* `limit`: Posts character limit (CW/Subject included in the counter).
|
||||
* `discription_limit`: The character limit for image descriptions.
|
||||
* `chat_limit`: Character limit of the instance chat messages.
|
||||
* `remote_limit`: Hard character limit beyond which remote posts will be dropped.
|
||||
* `upload_limit`: File size limit of uploads (except for avatar, background, banner).
|
||||
|
@ -36,7 +37,7 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
* `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.
|
||||
* `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance.
|
||||
* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
|
||||
* `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. See also: `restrict_unauthenticated`.
|
||||
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
|
||||
* `managed_config`: Whenether the config for pleroma-fe is configured in [:frontend_configurations](#frontend_configurations) or in ``static/config.json``.
|
||||
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML).
|
||||
|
@ -154,7 +155,7 @@ config :pleroma, :mrf_user_allowlist, %{
|
|||
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
|
||||
* `:reject` rejects the message entirely
|
||||
|
||||
#### mrf_steal_emoji
|
||||
#### :mrf_steal_emoji
|
||||
* `hosts`: List of hosts to steal emojis from
|
||||
* `rejected_shortcodes`: Regex-list of shortcodes to reject
|
||||
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
|
||||
|
@ -251,6 +252,7 @@ This section describe PWA manifest instance-specific values. Currently this opti
|
|||
* `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`).
|
||||
|
||||
## :emoji
|
||||
|
||||
* `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]`
|
||||
* `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]`
|
||||
* `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]`
|
||||
|
@ -259,13 +261,14 @@ This section describe PWA manifest instance-specific values. Currently this opti
|
|||
memory for this amount of seconds multiplied by the number of files.
|
||||
|
||||
## :media_proxy
|
||||
|
||||
* `enabled`: Enables proxying of remote media to the instance’s proxy
|
||||
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
|
||||
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
|
||||
* `whitelist`: List of domains to bypass the mediaproxy
|
||||
* `whitelist`: List of hosts with scheme to bypass the mediaproxy (e.g. `https://example.com`)
|
||||
* `invalidation`: options for remove media from cache after delete object:
|
||||
* `enabled`: Enables purge cache
|
||||
* `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use.
|
||||
* `enabled`: Enables purge cache
|
||||
* `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use.
|
||||
|
||||
### Purge cache strategy
|
||||
|
||||
|
@ -277,6 +280,7 @@ Urls of attachments pass to script as arguments.
|
|||
* `script_path`: path to external script.
|
||||
|
||||
Example:
|
||||
|
||||
```elixir
|
||||
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script,
|
||||
script_path: "./installation/nginx-cache-purge.example"
|
||||
|
@ -444,37 +448,32 @@ For each pool, the options are:
|
|||
|
||||
*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.
|
||||
It will increase memory usage, but federation would work faster.
|
||||
|
||||
* `:checkin_timeout` - timeout to checkin connection from pool. Default: 250ms.
|
||||
* `:max_connections` - maximum number of connections in the pool. Default: 250 connections.
|
||||
* `: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.
|
||||
* `:connection_acquisition_wait` - Timeout to acquire a connection from pool.The total max time is this value multiplied by the number of retries.
|
||||
* `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.
|
||||
* `:await_up_timeout` - Timeout to connect to the host.
|
||||
* `: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.
|
||||
|
||||
### :pools
|
||||
|
||||
*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:
|
||||
|
||||
* `:federation` for the federation 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
|
||||
* `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`)
|
||||
* `:default` for other requests
|
||||
* `: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.
|
||||
* `:media` - for rich media, media proxy.
|
||||
* `:upload` - for proxying media when a remote uploader is used and `proxy_remote: true`.
|
||||
* `:default` - for other requests.
|
||||
|
||||
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
|
||||
* `: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
|
||||
|
||||
|
@ -493,7 +492,7 @@ A built-in captcha provider. Enabled by default.
|
|||
#### Pleroma.Captcha.Kocaptcha
|
||||
|
||||
Kocaptcha is a very simple captcha service with a single API endpoint,
|
||||
the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint
|
||||
the source code is here: [kocaptcha](https://github.com/koto-bank/kocaptcha). The default endpoint
|
||||
`https://captcha.kotobank.ch` is hosted by the developer.
|
||||
|
||||
* `endpoint`: the Kocaptcha endpoint to use.
|
||||
|
@ -501,6 +500,7 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end
|
|||
## Uploads
|
||||
|
||||
### Pleroma.Upload
|
||||
|
||||
* `uploader`: Which one of the [uploaders](#uploaders) to use.
|
||||
* `filters`: List of [upload filters](#upload-filters) to use.
|
||||
* `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe`
|
||||
|
@ -513,10 +513,15 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end
|
|||
`strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
|
||||
|
||||
### Uploaders
|
||||
|
||||
#### Pleroma.Uploaders.Local
|
||||
|
||||
* `uploads`: Which directory to store the user-uploads in, relative to pleroma’s working directory.
|
||||
|
||||
#### Pleroma.Uploaders.S3
|
||||
|
||||
Don't forget to configure [Ex AWS S3](#ex-aws-s3-settings)
|
||||
|
||||
* `bucket`: S3 bucket name.
|
||||
* `bucket_namespace`: S3 bucket namespace.
|
||||
* `public_endpoint`: S3 endpoint that the user finally accesses(ex. "https://s3.dualstack.ap-northeast-1.amazonaws.com")
|
||||
|
@ -525,17 +530,23 @@ For example, when using CDN to S3 virtual host format, set "".
|
|||
At this time, write CNAME to CDN in public_endpoint.
|
||||
* `streaming_enabled`: Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems.
|
||||
|
||||
#### Ex AWS S3 settings
|
||||
|
||||
* `access_key_id`: Access key ID
|
||||
* `secret_access_key`: Secret access key
|
||||
* `host`: S3 host
|
||||
|
||||
Example:
|
||||
|
||||
```elixir
|
||||
config :ex_aws, :s3,
|
||||
access_key_id: "xxxxxxxxxx",
|
||||
secret_access_key: "yyyyyyyyyy",
|
||||
host: "s3.eu-central-1.amazonaws.com"
|
||||
```
|
||||
|
||||
### Upload filters
|
||||
|
||||
#### Pleroma.Upload.Filter.Mogrify
|
||||
|
||||
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`.
|
||||
|
||||
#### Pleroma.Upload.Filter.Dedupe
|
||||
|
||||
No specific configuration.
|
||||
|
||||
#### Pleroma.Upload.Filter.AnonymizeFilename
|
||||
|
||||
This filter replaces the filename (not the path) of an upload. For complete obfuscation, add
|
||||
|
@ -543,6 +554,20 @@ This filter replaces the filename (not the path) of an upload. For complete obfu
|
|||
|
||||
* `text`: Text to replace filenames in links. If empty, `{random}.extension` will be used. You can get the original filename extension by using `{extension}`, for example `custom-file-name.{extension}`.
|
||||
|
||||
#### Pleroma.Upload.Filter.Dedupe
|
||||
|
||||
No specific configuration.
|
||||
|
||||
#### Pleroma.Upload.Filter.Exiftool
|
||||
|
||||
This filter only strips the GPS and location metadata with Exiftool leaving color profiles and attributes intact.
|
||||
|
||||
No specific configuration.
|
||||
|
||||
#### Pleroma.Upload.Filter.Mogrify
|
||||
|
||||
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`.
|
||||
|
||||
## Email
|
||||
|
||||
### Pleroma.Emails.Mailer
|
||||
|
@ -603,8 +628,7 @@ Email notifications settings.
|
|||
Configuration options described in [Oban readme](https://github.com/sorentwo/oban#usage):
|
||||
|
||||
* `repo` - app's Ecto repo (`Pleroma.Repo`)
|
||||
* `verbose` - logs verbosity
|
||||
* `prune` - non-retryable jobs [pruning settings](https://github.com/sorentwo/oban#pruning) (`:disabled` / `{:maxlen, value}` / `{:maxage, value}`)
|
||||
* `log` - logs verbosity
|
||||
* `queues` - job queues (see below)
|
||||
* `crontab` - periodic jobs, see [`Oban.Cron`](#obancron)
|
||||
|
||||
|
@ -789,6 +813,8 @@ or
|
|||
curl -H "X-Admin-Token: somerandomtoken" "http://localhost:4000/api/pleroma/admin/users/invites"
|
||||
```
|
||||
|
||||
Warning: it's discouraged to use this feature because of the associated security risk: static / rarely changed instance-wide token is much weaker compared to email-password pair of a real admin user; consider using HTTP Basic Auth or OAuth-based authentication instead.
|
||||
|
||||
### :auth
|
||||
|
||||
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator.
|
||||
|
@ -970,11 +996,11 @@ config :pleroma, :database_config_whitelist, [
|
|||
|
||||
### :restrict_unauthenticated
|
||||
|
||||
Restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
|
||||
Restrict access for unauthenticated users to timelines (public and federated), user profiles and statuses.
|
||||
|
||||
* `timelines`: public and federated timelines
|
||||
* `local`: public timeline
|
||||
* `federated`
|
||||
* `federated`: federated timeline (includes public timeline)
|
||||
* `profiles`: user profiles
|
||||
* `local`
|
||||
* `remote`
|
||||
|
@ -982,7 +1008,14 @@ Restrict access for unauthenticated users to timelines (public and federate), us
|
|||
* `local`
|
||||
* `remote`
|
||||
|
||||
Note: setting `restrict_unauthenticated/timelines/local` to `true` has no practical sense if `restrict_unauthenticated/timelines/federated` is set to `false` (since local public activities will still be delivered to unauthenticated users as part of federated timeline).
|
||||
|
||||
## Pleroma.Web.ApiSpec.CastAndValidate
|
||||
|
||||
* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.
|
||||
|
||||
## :instances_favicons
|
||||
|
||||
Control favicons for instances.
|
||||
|
||||
* `enabled`: Allow/disallow displaying and getting instances favicons
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
# How to activate Pleroma in-database configuration
|
||||
## Explanation
|
||||
|
||||
The configuration of Pleroma has traditionally been managed with a config file, e.g. `config/prod.secret.exs`. This method requires a restart of the application for any configuration changes to take effect. We have made it possible to control most settings in the AdminFE interface after running a migration script.
|
||||
|
||||
## Migration to database config
|
||||
|
||||
1. Stop your Pleroma instance and edit your Pleroma config to enable database configuration:
|
||||
|
||||
```
|
||||
config :pleroma, configurable_from_database: true
|
||||
```
|
||||
|
||||
2. Run the mix task to migrate to the database. You'll receive some debugging output and a few messages informing you of what happened.
|
||||
|
||||
**Source:**
|
||||
|
||||
```
|
||||
$ mix pleroma.config migrate_to_db
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
**OTP:**
|
||||
|
||||
```
|
||||
$ ./bin/pleroma_ctl config migrate_to_db
|
||||
```
|
||||
|
||||
```
|
||||
10:04:34.155 [debug] QUERY OK source="config" db=1.6ms decode=2.0ms queue=33.5ms idle=0.0ms
|
||||
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
|
||||
Migrating settings from file: /home/pleroma/config/dev.secret.exs
|
||||
|
||||
10:04:34.240 [debug] QUERY OK db=4.5ms queue=0.3ms idle=92.2ms
|
||||
TRUNCATE config; []
|
||||
|
||||
10:04:34.244 [debug] QUERY OK db=2.8ms queue=0.3ms idle=97.2ms
|
||||
ALTER SEQUENCE config_id_seq RESTART; []
|
||||
|
||||
10:04:34.256 [debug] QUERY OK source="config" db=0.8ms queue=1.4ms idle=109.8ms
|
||||
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 WHERE ((c0."group" = $1) AND (c0."key" = $2)) [":pleroma", ":instance"]
|
||||
|
||||
10:04:34.292 [debug] QUERY OK db=2.6ms queue=1.7ms idle=137.7ms
|
||||
INSERT INTO "config" ("group","key","value","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5) RETURNING "id" [":pleroma", ":instance", <<131, 108, 0, 0, 0, 1, 104, 2, 100, 0, 4, 110, 97, 109, 101, 109, 0, 0, 0, 7, 66, 108, 101, 114, 111, 109, 97, 106>>, ~N[2020-07-12 15:04:34], ~N[2020-07-12 15:04:34]]
|
||||
Settings for key instance migrated.
|
||||
Settings for group :pleroma migrated.
|
||||
```
|
||||
|
||||
3. It is recommended to backup your config file now.
|
||||
```
|
||||
cp config/dev.secret.exs config/dev.secret.exs.orig
|
||||
```
|
||||
|
||||
4. Now you can edit your config file and strip it down to the only settings which are not possible to control in the database. e.g., the Postgres and webserver (Endpoint) settings cannot be controlled in the database because the application needs the settings to start up and access the database.
|
||||
|
||||
⚠️ **THIS IS NOT REQUIRED**
|
||||
|
||||
Any settings in the database will override those in the config file, but you may find it less confusing if the setting is only declared in one place.
|
||||
|
||||
A non-exhaustive list of settings that are only possible in the config file include the following:
|
||||
|
||||
* config :pleroma, Pleroma.Web.Endpoint
|
||||
* config :pleroma, Pleroma.Repo
|
||||
* config :pleroma, configurable_from_database
|
||||
* config :pleroma, :database, rum_enabled
|
||||
* config :pleroma, :connections_pool
|
||||
|
||||
Here is an example of a server config stripped down after migration:
|
||||
|
||||
```
|
||||
use Mix.Config
|
||||
|
||||
config :pleroma, Pleroma.Web.Endpoint,
|
||||
url: [host: "cool.pleroma.site", scheme: "https", port: 443]
|
||||
|
||||
|
||||
config :pleroma, Pleroma.Repo,
|
||||
adapter: Ecto.Adapters.Postgres,
|
||||
username: "pleroma",
|
||||
password: "MySecretPassword",
|
||||
database: "pleroma_prod",
|
||||
hostname: "localhost"
|
||||
|
||||
config :pleroma, configurable_from_database: true
|
||||
```
|
||||
|
||||
5. Start your instance back up and you can now access the Settings tab in AdminFE.
|
||||
|
||||
|
||||
## Reverting back from database config
|
||||
|
||||
1. Stop your Pleroma instance.
|
||||
|
||||
2. Run the mix task to migrate back from the database. You'll receive some debugging output and a few messages informing you of what happened.
|
||||
|
||||
**Source:**
|
||||
|
||||
```
|
||||
$ mix pleroma.config migrate_from_db
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
**OTP:**
|
||||
|
||||
```
|
||||
$ ./bin/pleroma_ctl config migrate_from_db
|
||||
```
|
||||
|
||||
```
|
||||
10:26:30.593 [debug] QUERY OK source="config" db=9.8ms decode=1.2ms queue=26.0ms idle=0.0ms
|
||||
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
|
||||
|
||||
10:26:30.659 [debug] QUERY OK source="config" db=1.1ms idle=80.7ms
|
||||
SELECT c0."id", c0."key", c0."group", c0."value", c0."inserted_at", c0."updated_at" FROM "config" AS c0 []
|
||||
Database configuration settings have been saved to config/dev.exported_from_db.secret.exs
|
||||
```
|
||||
|
||||
3. The in-database configuration still exists, but it will not be used if you remove `config :pleroma, configurable_from_database: true` from your config.
|
||||
|
||||
## Debugging
|
||||
|
||||
### Clearing database config
|
||||
You can clear the database config by truncating the `config` table in the database. e.g.,
|
||||
|
||||
```
|
||||
psql -d pleroma_dev
|
||||
pleroma_dev=# TRUNCATE config;
|
||||
TRUNCATE TABLE
|
||||
```
|
||||
|
||||
Additionally, every time you migrate the configuration to the database the config table is automatically truncated to ensure a clean migration.
|
||||
|
||||
### Manually removing a setting
|
||||
If you encounter a situation where the server cannot run properly because of an invalid setting in the database and this is preventing you from accessing AdminFE, you can manually remove the offending setting if you know which one it is.
|
||||
|
||||
e.g., here is an example showing a minimal configuration in the database. Only the `config :pleroma, :instance` settings are in the table:
|
||||
|
||||
```
|
||||
psql -d pleroma_dev
|
||||
pleroma_dev=# select * from config;
|
||||
id | key | value | inserted_at | updated_at | group
|
||||
----+-----------+------------------------------------------------------------+---------------------+---------------------+----------
|
||||
1 | :instance | \x836c0000000168026400046e616d656d00000007426c65726f6d616a | 2020-07-12 15:33:29 | 2020-07-12 15:33:29 | :pleroma
|
||||
(1 row)
|
||||
pleroma_dev=# delete from config where key = ':instance' and group = ':pleroma';
|
||||
DELETE 1
|
||||
```
|
||||
|
||||
Now the `config :pleroma, :instance` settings have been removed from the database.
|
|
@ -3,15 +3,48 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Mix.Pleroma do
|
||||
@apps [
|
||||
:restarter,
|
||||
:ecto,
|
||||
:ecto_sql,
|
||||
:postgrex,
|
||||
:db_connection,
|
||||
:cachex,
|
||||
:flake_id,
|
||||
:swoosh,
|
||||
:timex
|
||||
]
|
||||
@cachex_children ["object", "user"]
|
||||
@doc "Common functions to be reused in mix tasks"
|
||||
def start_pleroma do
|
||||
Pleroma.Config.Holder.save_default()
|
||||
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
|
||||
|
||||
if Pleroma.Config.get(:env) != :test do
|
||||
Application.put_env(:logger, :console, level: :debug)
|
||||
end
|
||||
|
||||
{:ok, _} = Application.ensure_all_started(:pleroma)
|
||||
apps =
|
||||
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do
|
||||
[:gun | @apps]
|
||||
else
|
||||
[:hackney | @apps]
|
||||
end
|
||||
|
||||
Enum.each(apps, &Application.ensure_all_started/1)
|
||||
|
||||
children = [
|
||||
Pleroma.Repo,
|
||||
{Pleroma.Config.TransferTask, false},
|
||||
Pleroma.Web.Endpoint
|
||||
]
|
||||
|
||||
cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
|
||||
|
||||
Supervisor.start_link(children ++ cachex_children,
|
||||
strategy: :one_for_one,
|
||||
name: Pleroma.Supervisor
|
||||
)
|
||||
|
||||
if Pleroma.Config.get(:env) not in [:test, :benchmark] do
|
||||
pleroma_rebooted?()
|
||||
|
|
|
@ -83,7 +83,7 @@ defp create(group, settings) do
|
|||
|
||||
defp migrate_from_db(opts) do
|
||||
if Pleroma.Config.get([:configurable_from_database]) do
|
||||
env = opts[:env] || "prod"
|
||||
env = opts[:env] || Pleroma.Config.get(:env)
|
||||
|
||||
config_path =
|
||||
if Pleroma.Config.get(:release) do
|
||||
|
@ -105,6 +105,10 @@ defp migrate_from_db(opts) do
|
|||
|
||||
:ok = File.close(file)
|
||||
System.cmd("mix", ["format", config_path])
|
||||
|
||||
shell_info(
|
||||
"Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs"
|
||||
)
|
||||
else
|
||||
migration_error()
|
||||
end
|
||||
|
@ -112,7 +116,7 @@ defp migrate_from_db(opts) do
|
|||
|
||||
defp migration_error do
|
||||
shell_error(
|
||||
"Migration is not allowed in config. You can change this behavior by setting `configurable_from_database` to true."
|
||||
"Migration is not allowed in config. You can change this behavior by setting `config :pleroma, configurable_from_database: true`"
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ def run(["gen" | rest]) do
|
|||
options,
|
||||
:uploads_dir,
|
||||
"What directory should media uploads go in (when using the local uploader)?",
|
||||
Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
|
||||
Config.get([Pleroma.Uploaders.Local, :uploads])
|
||||
)
|
||||
|> Path.expand()
|
||||
|
||||
|
@ -154,7 +154,7 @@ def run(["gen" | rest]) do
|
|||
options,
|
||||
:static_dir,
|
||||
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
|
||||
Pleroma.Config.get([:instance, :static_dir])
|
||||
Config.get([:instance, :static_dir])
|
||||
)
|
||||
|> Path.expand()
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ defmodule Mix.Tasks.Pleroma.NotificationSettings do
|
|||
@moduledoc """
|
||||
Example:
|
||||
|
||||
> mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588" # set false only for parallel588 user
|
||||
> mix pleroma.notification_settings --privacy-option=true # set true for all users
|
||||
> mix pleroma.notification_settings --hide-notification-contents=false --nickname-users="parallel588" # set false only for parallel588 user
|
||||
> mix pleroma.notification_settings --hide-notification-contents=true # set true for all users
|
||||
|
||||
"""
|
||||
|
||||
|
@ -19,16 +19,16 @@ def run(args) do
|
|||
OptionParser.parse(
|
||||
args,
|
||||
strict: [
|
||||
privacy_option: :boolean,
|
||||
hide_notification_contents: :boolean,
|
||||
email_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
|
||||
privacy_option
|
||||
if not is_nil(hide_notification_contents) do
|
||||
hide_notification_contents
|
||||
|> build_query(options)
|
||||
|> Pleroma.Repo.update_all([])
|
||||
end
|
||||
|
@ -36,15 +36,15 @@ def run(args) do
|
|||
shell_info("Done")
|
||||
end
|
||||
|
||||
defp build_query(privacy_option, options) do
|
||||
defp build_query(hide_notification_contents, options) do
|
||||
query =
|
||||
from(u in Pleroma.User,
|
||||
update: [
|
||||
set: [
|
||||
notification_settings:
|
||||
fragment(
|
||||
"jsonb_set(notification_settings, '{privacy_option}', ?)",
|
||||
^privacy_option
|
||||
"jsonb_set(notification_settings, '{hide_notification_contents}', ?)",
|
||||
^hide_notification_contents
|
||||
)
|
||||
]
|
||||
]
|
||||
|
|
|
@ -232,7 +232,7 @@ def run(["tag", nickname | tags]) do
|
|||
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||
user = user |> User.tag(tags)
|
||||
|
||||
shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
|
||||
shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
|
||||
else
|
||||
_ ->
|
||||
shell_error("Could not change user tags for #{nickname}")
|
||||
|
@ -245,7 +245,7 @@ def run(["untag", nickname | tags]) do
|
|||
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||
user = user |> User.untag(tags)
|
||||
|
||||
shell_info("Tags of #{user.nickname}: #{inspect(tags)}")
|
||||
shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
|
||||
else
|
||||
_ ->
|
||||
shell_error("Could not change user tags for #{nickname}")
|
||||
|
|
|
@ -35,13 +35,19 @@ def user_agent do
|
|||
# See http://elixir-lang.org/docs/stable/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
def start(_type, _args) do
|
||||
Pleroma.Config.Holder.save_default()
|
||||
# Scrubbers are compiled at runtime and therefore will cause a conflict
|
||||
# every time the application is restarted, so we disable module
|
||||
# conflicts at runtime
|
||||
Code.compiler_options(ignore_module_conflict: true)
|
||||
Pleroma.Telemetry.Logger.attach()
|
||||
Config.Holder.save_default()
|
||||
Pleroma.HTML.compile_scrubbers()
|
||||
Config.DeprecationWarnings.warn()
|
||||
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
|
||||
Pleroma.ApplicationRequirements.verify!()
|
||||
setup_instrumenters()
|
||||
load_custom_modules()
|
||||
Pleroma.Docs.JSON.compile()
|
||||
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
|
||||
|
@ -162,7 +168,8 @@ defp idempotency_expiration,
|
|||
defp seconds_valid_interval,
|
||||
do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
|
||||
|
||||
defp build_cachex(type, opts),
|
||||
@spec build_cachex(String.t(), keyword()) :: map()
|
||||
def build_cachex(type, opts),
|
||||
do: %{
|
||||
id: String.to_atom("cachex_" <> type),
|
||||
start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
|
||||
|
@ -217,9 +224,7 @@ defp task_children(_) do
|
|||
|
||||
# start hackney and gun pools in tests
|
||||
defp http_children(_, :test) do
|
||||
hackney_options = Config.get([:hackney_pools, :federation])
|
||||
hackney_pool = :hackney_pool.child_spec(:federation, hackney_options)
|
||||
[hackney_pool, Pleroma.Pool.Supervisor]
|
||||
http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil)
|
||||
end
|
||||
|
||||
defp http_children(Tesla.Adapter.Hackney, _) do
|
||||
|
@ -238,7 +243,10 @@ defp http_children(Tesla.Adapter.Hackney, _) do
|
|||
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: []
|
||||
end
|
||||
|
|
|
@ -54,6 +54,7 @@ def warn do
|
|||
check_hellthread_threshold()
|
||||
mrf_user_allowlist()
|
||||
check_old_mrf_config()
|
||||
check_media_proxy_whitelist_config()
|
||||
end
|
||||
|
||||
def check_old_mrf_config do
|
||||
|
@ -65,7 +66,7 @@ def check_old_mrf_config do
|
|||
move_namespace_and_warn(@mrf_config_map, warning_preface)
|
||||
end
|
||||
|
||||
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok
|
||||
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil
|
||||
def move_namespace_and_warn(config_map, warning_preface) do
|
||||
warning =
|
||||
Enum.reduce(config_map, "", fn
|
||||
|
@ -84,4 +85,16 @@ def move_namespace_and_warn(config_map, warning_preface) do
|
|||
Logger.warn(warning_preface <> warning)
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_media_proxy_whitelist_config() :: :ok | nil
|
||||
def check_media_proxy_whitelist_config do
|
||||
whitelist = Config.get([:media_proxy, :whitelist])
|
||||
|
||||
if Enum.any?(whitelist, &(not String.starts_with?(&1, "http"))) do
|
||||
Logger.warn("""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your config is using old format (only domain) for MediaProxy whitelist option. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
|
||||
""")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,6 +12,11 @@ defmodule Pleroma.Config.Loader do
|
|||
:swarm
|
||||
]
|
||||
|
||||
@reject_groups [
|
||||
:postgrex,
|
||||
:tesla
|
||||
]
|
||||
|
||||
if Code.ensure_loaded?(Config.Reader) do
|
||||
@reader Config.Reader
|
||||
|
||||
|
@ -47,7 +52,8 @@ defp filter(configs) do
|
|||
@spec filter_group(atom(), keyword()) :: keyword()
|
||||
def filter_group(group, configs) do
|
||||
Enum.reject(configs[group], fn {key, _v} ->
|
||||
key in @reject_keys or (group == :phoenix and key == :serve_endpoints) or group == :postgrex
|
||||
key in @reject_keys or group in @reject_groups or
|
||||
(group == :phoenix and key == :serve_endpoints)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,8 +31,8 @@ defmodule Pleroma.Config.TransferTask do
|
|||
{:pleroma, :gopher, [:enabled]}
|
||||
]
|
||||
|
||||
def start_link(_) do
|
||||
load_and_update_env()
|
||||
def start_link(restart_pleroma? \\ true) do
|
||||
load_and_update_env([], restart_pleroma?)
|
||||
if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
|
||||
:ignore
|
||||
end
|
||||
|
|
|
@ -6,16 +6,21 @@ def process(implementation, descriptions) do
|
|||
implementation.process(descriptions)
|
||||
end
|
||||
|
||||
@spec list_modules_in_dir(String.t(), String.t()) :: [module()]
|
||||
def list_modules_in_dir(dir, start) do
|
||||
with {:ok, files} <- File.ls(dir) do
|
||||
files
|
||||
|> Enum.filter(&String.ends_with?(&1, ".ex"))
|
||||
|> Enum.map(fn filename ->
|
||||
module = filename |> String.trim_trailing(".ex") |> Macro.camelize()
|
||||
String.to_atom(start <> module)
|
||||
end)
|
||||
end
|
||||
@spec list_behaviour_implementations(behaviour :: module()) :: [module()]
|
||||
def list_behaviour_implementations(behaviour) do
|
||||
:code.all_loaded()
|
||||
|> Enum.filter(fn {module, _} ->
|
||||
# This shouldn't be needed as all modules are expected to have module_info/1,
|
||||
# but in test enviroments some transient modules `:elixir_compiler_XX`
|
||||
# are loaded for some reason (where XX is a random integer).
|
||||
if function_exported?(module, :module_info, 1) do
|
||||
module.module_info(:attributes)
|
||||
|> Keyword.get_values(:behaviour)
|
||||
|> List.flatten()
|
||||
|> Enum.member?(behaviour)
|
||||
end
|
||||
end)
|
||||
|> Enum.map(fn {module, _} -> module end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -87,6 +92,12 @@ defp humanize(entity) do
|
|||
else: string
|
||||
end
|
||||
|
||||
defp format_suggestions({:list_behaviour_implementations, behaviour}) do
|
||||
behaviour
|
||||
|> list_behaviour_implementations()
|
||||
|> format_suggestions()
|
||||
end
|
||||
|
||||
defp format_suggestions([]), do: []
|
||||
|
||||
defp format_suggestions([suggestion | tail]) do
|
||||
|
|
|
@ -1,5 +1,19 @@
|
|||
defmodule Pleroma.Docs.JSON do
|
||||
@behaviour Pleroma.Docs.Generator
|
||||
@external_resource "config/description.exs"
|
||||
@raw_config Pleroma.Config.Loader.read("config/description.exs")
|
||||
@raw_descriptions @raw_config[:pleroma][:config_description]
|
||||
@term __MODULE__.Compiled
|
||||
|
||||
@spec compile :: :ok
|
||||
def compile do
|
||||
:persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(@raw_descriptions))
|
||||
end
|
||||
|
||||
@spec compiled_descriptions :: Map.t()
|
||||
def compiled_descriptions do
|
||||
:persistent_term.get(@term)
|
||||
end
|
||||
|
||||
@spec process(keyword()) :: {:ok, String.t()}
|
||||
def process(descriptions) do
|
||||
|
@ -13,11 +27,4 @@ def process(descriptions) do
|
|||
{:ok, path}
|
||||
end
|
||||
end
|
||||
|
||||
def compile do
|
||||
with config <- Pleroma.Config.Loader.read("config/description.exs") do
|
||||
config[:pleroma][:config_description]
|
||||
|> Pleroma.Docs.Generator.convert_to_strings()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -68,6 +68,11 @@ defp print_suggestion(file, suggestion, as_list \\ false) do
|
|||
IO.write(file, " #{list_mark}`#{inspect(suggestion)}`\n")
|
||||
end
|
||||
|
||||
defp print_suggestions(file, {:list_behaviour_implementations, behaviour}) do
|
||||
suggestions = Pleroma.Docs.Generator.list_behaviour_implementations(behaviour)
|
||||
print_suggestions(file, suggestions)
|
||||
end
|
||||
|
||||
defp print_suggestions(_file, nil), do: nil
|
||||
|
||||
defp print_suggestions(_file, ""), do: nil
|
||||
|
|
|
@ -10,7 +10,7 @@ defmodule Pleroma.Emails.AdminEmail do
|
|||
alias Pleroma.Config
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
defp instance_config, do: Pleroma.Config.get(:instance)
|
||||
defp instance_config, do: Config.get(:instance)
|
||||
defp instance_name, do: instance_config()[:name]
|
||||
|
||||
defp instance_notify_email do
|
||||
|
@ -72,6 +72,8 @@ def report(to, reporter, account, statuses, comment) do
|
|||
<p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p>
|
||||
#{comment_html}
|
||||
#{statuses_html}
|
||||
<p>
|
||||
<a href="#{Pleroma.Web.base_url()}/pleroma/admin/#/reports/index">View Reports in AdminFE</a>
|
||||
"""
|
||||
|
||||
new()
|
||||
|
|
|
@ -108,7 +108,7 @@ defp load_pack(pack_dir, emoji_groups) do
|
|||
if File.exists?(emoji_txt) do
|
||||
load_from_file(emoji_txt, emoji_groups)
|
||||
else
|
||||
extensions = Pleroma.Config.get([:emoji, :pack_extensions])
|
||||
extensions = Config.get([:emoji, :pack_extensions])
|
||||
|
||||
Logger.info(
|
||||
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{
|
||||
|
|
|
@ -34,10 +34,18 @@ def get(id, %{id: user_id} = _user) do
|
|||
Repo.one(query)
|
||||
end
|
||||
|
||||
def get_filters(%User{id: user_id} = _user) do
|
||||
def get_active(query) do
|
||||
from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now())
|
||||
end
|
||||
|
||||
def get_irreversible(query) do
|
||||
from(f in query, where: f.hide)
|
||||
end
|
||||
|
||||
def get_filters(query \\ __MODULE__, %User{id: user_id}) do
|
||||
query =
|
||||
from(
|
||||
f in Pleroma.Filter,
|
||||
f in query,
|
||||
where: f.user_id == ^user_id,
|
||||
order_by: [desc: :id]
|
||||
)
|
||||
|
@ -95,4 +103,34 @@ def update(%Pleroma.Filter{} = filter, params) do
|
|||
|> validate_required([:phrase, :context])
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def compose_regex(user_or_filters, format \\ :postgres)
|
||||
|
||||
def compose_regex(%User{} = user, format) do
|
||||
__MODULE__
|
||||
|> get_active()
|
||||
|> get_irreversible()
|
||||
|> get_filters(user)
|
||||
|> compose_regex(format)
|
||||
end
|
||||
|
||||
def compose_regex([_ | _] = filters, format) do
|
||||
phrases =
|
||||
filters
|
||||
|> Enum.map(& &1.phrase)
|
||||
|> Enum.join("|")
|
||||
|
||||
case format do
|
||||
:postgres ->
|
||||
"\\y(#{phrases})\\y"
|
||||
|
||||
:re ->
|
||||
~r/\b#{phrases}\b/i
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def compose_regex(_, _), do: nil
|
||||
end
|
||||
|
|
|
@ -19,7 +19,8 @@ defmodule Pleroma.Gun.API do
|
|||
:tls_opts,
|
||||
:tcp_opts,
|
||||
:socks_opts,
|
||||
:ws_opts
|
||||
:ws_opts,
|
||||
:supervise
|
||||
]
|
||||
|
||||
@impl Gun
|
||||
|
|
|
@ -3,85 +3,33 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Gun.Conn do
|
||||
@moduledoc """
|
||||
Struct for gun connection data
|
||||
"""
|
||||
alias Pleroma.Gun
|
||||
alias Pleroma.Pool.Connections
|
||||
|
||||
require Logger
|
||||
|
||||
@type gun_state :: :up | :down
|
||||
@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
|
||||
def open(%URI{} = uri, opts) do
|
||||
pool_opts = Pleroma.Config.get([:connections_pool], [])
|
||||
|
||||
opts =
|
||||
opts
|
||||
|> 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(:supervise, false)
|
||||
|> maybe_add_tls_opts(uri)
|
||||
|
||||
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
|
||||
|
||||
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
|
||||
do_open(uri, opts)
|
||||
end
|
||||
|
||||
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 = [
|
||||
verify: :verify_peer,
|
||||
cacertfile: CAStore.file_path(),
|
||||
depth: 20,
|
||||
reuse_sessions: false,
|
||||
verify_fun:
|
||||
{&:ssl_verify_hostname.verify_fun/3,
|
||||
[check_hostname: Pleroma.HTTP.Connection.format_host(host)]}
|
||||
log_level: :warning,
|
||||
customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)]
|
||||
]
|
||||
|
||||
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]),
|
||||
stream <- Gun.connect(conn, connect_opts),
|
||||
{:response, :fin, 200, _} <- Gun.await(conn, stream) do
|
||||
conn
|
||||
{:ok, conn}
|
||||
else
|
||||
error ->
|
||||
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),
|
||||
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
|
||||
conn
|
||||
{:ok, conn}
|
||||
else
|
||||
error ->
|
||||
Logger.warn(
|
||||
|
@ -155,11 +103,11 @@ defp do_open(uri, %{proxy: {proxy_type, proxy_host, proxy_port}} = opts) do
|
|||
end
|
||||
|
||||
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),
|
||||
{:ok, _} <- Gun.await_up(conn, opts[:await_up_timeout]) do
|
||||
conn
|
||||
{:ok, conn}
|
||||
else
|
||||
error ->
|
||||
Logger.warn(
|
||||
|
@ -171,7 +119,7 @@ defp do_open(%URI{host: host, port: port} = uri, opts) do
|
|||
end
|
||||
|
||||
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}
|
||||
end
|
||||
|
||||
|
@ -181,17 +129,6 @@ defp add_http2_opts(opts, "https", tls_opts) do
|
|||
|
||||
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
|
||||
"#{scheme}://#{host}#{path}"
|
||||
end
|
||||
|
|
|
@ -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.cast(worker_pid, {:add_client, self(), false})
|
||||
{: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(), true})
|
||||
|
||||
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.cast(worker_pid, {:remove_client, self()})
|
||||
|
||||
[] ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -0,0 +1,127 @@
|
|||
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, send_pid_back}, %{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)
|
||||
|
||||
if send_pid_back, do: send(client_pid, {:conn_pid, conn_pid})
|
||||
|
||||
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)
|
||||
{:noreply, state, :hibernate}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({: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
|
||||
|
||||
{:noreply, %{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
|
||||
# Sometimes the client is dead before we demonitor it in :remove_client, so the message
|
||||
# arrives anyway
|
||||
|
||||
case state.client_monitors[pid] do
|
||||
nil ->
|
||||
{:noreply, state, :hibernate}
|
||||
|
||||
_ref ->
|
||||
:telemetry.execute(
|
||||
[:pleroma, :connection_pool, :client_death],
|
||||
%{client_pid: pid, reason: reason},
|
||||
%{key: state.key}
|
||||
)
|
||||
|
||||
handle_cast({:remove_client, pid}, state)
|
||||
end
|
||||
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
|
|
@ -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
|
||||
|
||||
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 ::
|
||||
{Connection.host(), pos_integer()}
|
||||
| {Connection.proxy_type(), Connection.host(), pos_integer()}
|
||||
|
||||
@callback options(keyword(), URI.t()) :: keyword()
|
||||
@callback after_request(keyword()) :: :ok
|
||||
|
||||
@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
|
||||
@callback get_conn(URI.t(), keyword()) :: {:ok, term()} | {:error, term()}
|
||||
|
||||
@spec format_proxy(String.t() | tuple() | nil) :: proxy() | nil
|
||||
def format_proxy(nil), do: nil
|
||||
|
||||
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, type, host, port} -> {type, host, port}
|
||||
_ -> nil
|
||||
|
@ -38,4 +36,105 @@ def format_proxy(proxy_url) do
|
|||
@spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword()
|
||||
def maybe_add_proxy(opts, nil), do: opts
|
||||
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
|
||||
|
|
|
@ -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
|
||||
@behaviour Pleroma.HTTP.AdapterHelper
|
||||
|
||||
alias Pleroma.Gun.ConnectionPool
|
||||
alias Pleroma.HTTP.AdapterHelper
|
||||
alias Pleroma.Pool.Connections
|
||||
|
||||
require Logger
|
||||
|
||||
|
@ -14,7 +14,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do
|
|||
connect_timeout: 5_000,
|
||||
domain_lookup_timeout: 5_000,
|
||||
tls_handshake_timeout: 5_000,
|
||||
retry: 1,
|
||||
retry: 0,
|
||||
retry_timeout: 1000,
|
||||
await_up_timeout: 5_000
|
||||
]
|
||||
|
@ -31,16 +31,7 @@ def options(incoming_opts \\ [], %URI{} = uri) do
|
|||
|> Keyword.merge(config_opts)
|
||||
|> add_scheme_opts(uri)
|
||||
|> AdapterHelper.maybe_add_proxy(proxy)
|
||||
|> maybe_get_conn(uri, 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
|
||||
|> Keyword.merge(incoming_opts)
|
||||
end
|
||||
|
||||
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
|
||||
opts
|
||||
|> Keyword.put(:certificates_verification, true)
|
||||
|> Keyword.put(:tls_opts, log_level: :warning)
|
||||
end
|
||||
|
||||
defp maybe_get_conn(adapter_opts, uri, incoming_opts) do
|
||||
{receive_conn?, opts} =
|
||||
adapter_opts
|
||||
|> Keyword.merge(incoming_opts)
|
||||
|> Keyword.pop(:receive_conn, true)
|
||||
|
||||
if Connections.alive?(:gun_connections) and receive_conn? do
|
||||
checkin_conn(uri, opts)
|
||||
else
|
||||
opts
|
||||
@spec get_conn(URI.t(), keyword()) :: {:ok, keyword()} | {:error, atom()}
|
||||
def get_conn(uri, opts) do
|
||||
case ConnectionPool.get_conn(uri, opts) do
|
||||
{:ok, conn_pid} -> {:ok, Keyword.merge(opts, conn: conn_pid, close_conn: false)}
|
||||
err -> err
|
||||
end
|
||||
end
|
||||
|
||||
defp checkin_conn(uri, opts) do
|
||||
case Connections.checkin(uri, :gun_connections) do
|
||||
nil ->
|
||||
Task.start(Pleroma.Gun.Conn, :open, [uri, :gun_connections, opts])
|
||||
opts
|
||||
@prefix Pleroma.Gun.ConnectionPool
|
||||
def limiter_setup do
|
||||
wait = Pleroma.Config.get([:connections_pool, :connection_acquisition_wait])
|
||||
retries = Pleroma.Config.get([:connections_pool, :connection_acquisition_retries])
|
||||
|
||||
conn when is_pid(conn) ->
|
||||
Keyword.merge(opts, conn: conn, close_conn: false)
|
||||
end
|
||||
:pools
|
||||
|> Pleroma.Config.get([])
|
||||
|> 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
|
||||
|
|
|
@ -24,5 +24,6 @@ def options(connection_opts \\ [], %URI{} = uri) do
|
|||
|
||||
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
|
||||
|
|
|
@ -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`.
|
||||
"""
|
||||
|
||||
alias Pleroma.HTTP.Connection
|
||||
alias Pleroma.HTTP.AdapterHelper
|
||||
alias Pleroma.HTTP.Request
|
||||
alias Pleroma.HTTP.RequestBuilder, as: Builder
|
||||
alias Tesla.Client
|
||||
|
@ -60,49 +60,30 @@ def post(url, body, headers \\ [], options \\ []),
|
|||
{:ok, Env.t()} | {:error, any()}
|
||||
def request(method, url, body, headers, options) when is_binary(url) do
|
||||
uri = URI.parse(url)
|
||||
adapter_opts = Connection.options(uri, options[:adapter] || [])
|
||||
options = put_in(options[:adapter], adapter_opts)
|
||||
params = options[:params] || []
|
||||
request = build_request(method, headers, options, url, body, params)
|
||||
adapter_opts = AdapterHelper.options(uri, options[:adapter] || [])
|
||||
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
client = Tesla.client([Tesla.Middleware.FollowRedirects], adapter)
|
||||
case AdapterHelper.get_conn(uri, adapter_opts) do
|
||||
{: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? =
|
||||
if adapter == Tesla.Adapter.Gun && pid do
|
||||
Process.alive?(pid)
|
||||
else
|
||||
false
|
||||
end
|
||||
client = Tesla.client(adapter_middlewares(adapter), adapter)
|
||||
|
||||
request_opts =
|
||||
adapter_opts
|
||||
|> Enum.into(%{})
|
||||
|> Map.put(:env, Pleroma.Config.get([:env]))
|
||||
|> Map.put(:pool_alive?, pool_alive?)
|
||||
maybe_limit(
|
||||
fn ->
|
||||
request(client, request)
|
||||
end,
|
||||
adapter,
|
||||
adapter_opts
|
||||
)
|
||||
|
||||
response = request(client, request, request_opts)
|
||||
|
||||
Connection.after_request(adapter_opts)
|
||||
|
||||
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
|
||||
)
|
||||
# Connection release is handled in a custom FollowRedirects middleware
|
||||
err ->
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@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.convert_to_keyword()
|
||||
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
|
||||
|
|
|
@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do
|
|||
schema "instances" do
|
||||
field(:host, :string)
|
||||
field(:unreachable_since, :naive_datetime_usec)
|
||||
field(:favicon, :string)
|
||||
field(:favicon_updated_at, :naive_datetime)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do
|
|||
|
||||
def changeset(struct, params \\ %{}) do
|
||||
struct
|
||||
|> cast(params, [:host, :unreachable_since])
|
||||
|> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])
|
||||
|> validate_required([:host])
|
||||
|> unique_constraint(:host)
|
||||
end
|
||||
|
@ -120,4 +122,48 @@ defp parse_datetime(datetime) when is_binary(datetime) do
|
|||
end
|
||||
|
||||
defp parse_datetime(datetime), do: datetime
|
||||
|
||||
def get_or_update_favicon(%URI{host: host} = instance_uri) do
|
||||
existing_record = Repo.get_by(Instance, %{host: host})
|
||||
now = NaiveDateTime.utc_now()
|
||||
|
||||
if existing_record && existing_record.favicon_updated_at &&
|
||||
NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do
|
||||
existing_record.favicon
|
||||
else
|
||||
favicon = scrape_favicon(instance_uri)
|
||||
|
||||
if existing_record do
|
||||
existing_record
|
||||
|> changeset(%{favicon: favicon, favicon_updated_at: now})
|
||||
|> Repo.update()
|
||||
else
|
||||
%Instance{}
|
||||
|> changeset(%{host: host, favicon: favicon, favicon_updated_at: now})
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
favicon
|
||||
end
|
||||
end
|
||||
|
||||
defp scrape_favicon(%URI{} = instance_uri) do
|
||||
try do
|
||||
with {:ok, %Tesla.Env{body: html}} <-
|
||||
Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]),
|
||||
favicon_rel <-
|
||||
html
|
||||
|> Floki.parse_document!()
|
||||
|> Floki.attribute("link[rel=icon]", "href")
|
||||
|> List.first(),
|
||||
favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(),
|
||||
true <- is_binary(favicon) do
|
||||
favicon
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
rescue
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MigrationHelper.NotificationBackfill do
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
@ -25,18 +24,27 @@ def fill_in_notification_types do
|
|||
|> type_from_activity()
|
||||
|
||||
notification
|
||||
|> Notification.changeset(%{type: type})
|
||||
|> Ecto.Changeset.change(%{type: type})
|
||||
|> Repo.update()
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_by_ap_id(ap_id) do
|
||||
q =
|
||||
from(u in User,
|
||||
select: u.id
|
||||
)
|
||||
|
||||
Repo.get_by(q, ap_id: ap_id)
|
||||
end
|
||||
|
||||
# This is copied over from Notifications to keep this stable.
|
||||
defp type_from_activity(%{data: %{"type" => type}} = activity) do
|
||||
case type do
|
||||
"Follow" ->
|
||||
accepted_function = fn activity ->
|
||||
with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]),
|
||||
%User{} = followed <- User.get_by_ap_id(activity.data["object"]) do
|
||||
with %User{} = follower <- get_by_ap_id(activity.data["actor"]),
|
||||
%User{} = followed <- get_by_ap_id(activity.data["object"]) do
|
||||
Pleroma.FollowingRelationship.following?(follower, followed)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -130,6 +130,7 @@ def for_user_query(user, opts \\ %{}) do
|
|||
|> preload([n, a, o], activity: {a, object: o})
|
||||
|> exclude_notification_muted(user, exclude_notification_muted_opts)
|
||||
|> exclude_blocked(user, exclude_blocked_opts)
|
||||
|> exclude_filtered(user)
|
||||
|> exclude_visibility(opts)
|
||||
end
|
||||
|
||||
|
@ -158,6 +159,20 @@ defp exclude_notification_muted(query, user, opts) do
|
|||
|> where([n, a, o, tm], is_nil(tm.user_id))
|
||||
end
|
||||
|
||||
defp exclude_filtered(query, user) do
|
||||
case Pleroma.Filter.compose_regex(user) do
|
||||
nil ->
|
||||
query
|
||||
|
||||
regex ->
|
||||
from([_n, a, o] in query,
|
||||
where:
|
||||
fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
|
||||
fragment("?->>'actor' = ?", o.data, ^user.ap_id)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@valid_visibilities ~w[direct unlisted public private]
|
||||
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
|
@ -337,6 +352,7 @@ def dismiss(%{id: user_id} = _user, id) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
|
||||
def create_notifications(activity, options \\ [])
|
||||
|
||||
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
|
||||
|
@ -481,6 +497,10 @@ def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_i
|
|||
end
|
||||
end
|
||||
|
||||
def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
|
||||
[object_id]
|
||||
end
|
||||
|
||||
def get_potential_receiver_ap_ids(activity) do
|
||||
[]
|
||||
|> Utils.maybe_notify_to_recipients(activity)
|
||||
|
@ -551,11 +571,9 @@ def skip?(%Activity{} = activity, %User{} = user) do
|
|||
[
|
||||
:self,
|
||||
:invisible,
|
||||
:followers,
|
||||
:follows,
|
||||
:non_followers,
|
||||
:non_follows,
|
||||
:recently_followed
|
||||
:block_from_strangers,
|
||||
:recently_followed,
|
||||
:filtered
|
||||
]
|
||||
|> Enum.find(&skip?(&1, activity, user))
|
||||
end
|
||||
|
@ -574,45 +592,15 @@ def skip?(:invisible, %Activity{} = activity, _) do
|
|||
end
|
||||
|
||||
def skip?(
|
||||
:followers,
|
||||
:block_from_strangers,
|
||||
%Activity{} = activity,
|
||||
%User{notification_settings: %{followers: false}} = 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
|
||||
%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?(
|
||||
: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
|
||||
def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
|
||||
actor = activity.data["actor"]
|
||||
|
@ -624,6 +612,26 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity,
|
|||
end)
|
||||
end
|
||||
|
||||
def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false
|
||||
|
||||
def skip?(:filtered, activity, user) do
|
||||
object = Object.normalize(activity)
|
||||
|
||||
cond do
|
||||
is_nil(object) ->
|
||||
false
|
||||
|
||||
object.data["actor"] == user.ap_id ->
|
||||
false
|
||||
|
||||
not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
|
||||
Regex.match?(regex, object.data["content"])
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def skip?(_, _, _), do: false
|
||||
|
||||
def for_user_and_activity(user, activity) do
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Plugs.RateLimiter
|
||||
alias Pleroma.User
|
||||
|
||||
def init(options) do
|
||||
|
@ -11,7 +14,10 @@ def init(options) do
|
|||
end
|
||||
|
||||
def secret_token do
|
||||
Pleroma.Config.get(:admin_token)
|
||||
case Pleroma.Config.get(:admin_token) do
|
||||
blank when blank in [nil, ""] -> nil
|
||||
token -> token
|
||||
end
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
||||
|
@ -26,9 +32,9 @@ def call(conn, _) do
|
|||
|
||||
def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
|
||||
if admin_token == secret_token() do
|
||||
assign(conn, :user, %User{is_admin: true})
|
||||
assign_admin_user(conn)
|
||||
else
|
||||
conn
|
||||
handle_bad_token(conn)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -36,8 +42,19 @@ def authenticate(conn) do
|
|||
token = secret_token()
|
||||
|
||||
case get_req_header(conn, "x-admin-token") do
|
||||
[^token] -> assign(conn, :user, %User{is_admin: true})
|
||||
_ -> conn
|
||||
blank when blank in [[], [""]] -> conn
|
||||
[^token] -> assign_admin_user(conn)
|
||||
_ -> handle_bad_token(conn)
|
||||
end
|
||||
end
|
||||
|
||||
defp assign_admin_user(conn) do
|
||||
conn
|
||||
|> assign(:user, %User{is_admin: true})
|
||||
|> OAuthScopesPlug.skip_plug()
|
||||
end
|
||||
|
||||
defp handle_bad_token(conn) do
|
||||
RateLimiter.call(conn, name: :authentication)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -69,10 +69,11 @@ defp csp_string do
|
|||
img_src = "img-src 'self' data: blob:"
|
||||
media_src = "media-src 'self'"
|
||||
|
||||
# Strict multimedia CSP enforcement only when MediaProxy is enabled
|
||||
{img_src, media_src} =
|
||||
if Config.get([:media_proxy, :enabled]) &&
|
||||
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
|
||||
sources = get_proxy_and_attachment_sources()
|
||||
sources = build_csp_multimedia_source_list()
|
||||
{[img_src, sources], [media_src, sources]}
|
||||
else
|
||||
{[img_src, " https:"], [media_src, " https:"]}
|
||||
|
@ -81,14 +82,14 @@ defp csp_string do
|
|||
connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
|
||||
|
||||
connect_src =
|
||||
if Pleroma.Config.get(:env) == :dev do
|
||||
if Config.get(:env) == :dev do
|
||||
[connect_src, " http://localhost:3035/"]
|
||||
else
|
||||
connect_src
|
||||
end
|
||||
|
||||
script_src =
|
||||
if Pleroma.Config.get(:env) == :dev do
|
||||
if Config.get(:env) == :dev do
|
||||
"script-src 'self' 'unsafe-eval'"
|
||||
else
|
||||
"script-src 'self'"
|
||||
|
@ -107,38 +108,64 @@ defp csp_string do
|
|||
|> :erlang.iolist_to_binary()
|
||||
end
|
||||
|
||||
defp get_proxy_and_attachment_sources do
|
||||
defp build_csp_from_whitelist([], acc), do: acc
|
||||
|
||||
defp build_csp_from_whitelist([last], acc) do
|
||||
[build_csp_param_from_whitelist(last) | acc]
|
||||
end
|
||||
|
||||
defp build_csp_from_whitelist([head | tail], acc) do
|
||||
build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc])
|
||||
end
|
||||
|
||||
# TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist
|
||||
defp build_csp_param_from_whitelist("http" <> _ = url) do
|
||||
build_csp_param(url)
|
||||
end
|
||||
|
||||
defp build_csp_param_from_whitelist(url), do: url
|
||||
|
||||
defp build_csp_multimedia_source_list do
|
||||
media_proxy_whitelist =
|
||||
Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
|
||||
add_source(acc, host)
|
||||
end)
|
||||
[:media_proxy, :whitelist]
|
||||
|> Config.get()
|
||||
|> build_csp_from_whitelist([])
|
||||
|
||||
media_proxy_base_url =
|
||||
if Config.get([:media_proxy, :base_url]),
|
||||
do: URI.parse(Config.get([:media_proxy, :base_url])).host
|
||||
captcha_method = Config.get([Pleroma.Captcha, :method])
|
||||
captcha_endpoint = Config.get([captcha_method, :endpoint])
|
||||
|
||||
upload_base_url =
|
||||
if Config.get([Pleroma.Upload, :base_url]),
|
||||
do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host
|
||||
base_endpoints =
|
||||
[
|
||||
[:media_proxy, :base_url],
|
||||
[Pleroma.Upload, :base_url],
|
||||
[Pleroma.Uploaders.S3, :public_endpoint]
|
||||
]
|
||||
|> Enum.map(&Config.get/1)
|
||||
|
||||
s3_endpoint =
|
||||
if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3,
|
||||
do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host
|
||||
|
||||
[]
|
||||
|> add_source(media_proxy_base_url)
|
||||
|> add_source(upload_base_url)
|
||||
|> add_source(s3_endpoint)
|
||||
[captcha_endpoint | base_endpoints]
|
||||
|> Enum.map(&build_csp_param/1)
|
||||
|> Enum.reduce([], &add_source(&2, &1))
|
||||
|> add_source(media_proxy_whitelist)
|
||||
end
|
||||
|
||||
defp add_source(iodata, nil), do: iodata
|
||||
defp add_source(iodata, []), do: iodata
|
||||
defp add_source(iodata, source), do: [[?\s, source] | iodata]
|
||||
|
||||
defp add_csp_param(csp_iodata, nil), do: csp_iodata
|
||||
|
||||
defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
|
||||
|
||||
defp build_csp_param(nil), do: nil
|
||||
|
||||
defp build_csp_param(url) when is_binary(url) do
|
||||
%{host: host, scheme: scheme} = URI.parse(url)
|
||||
|
||||
if scheme do
|
||||
[scheme, "://", host]
|
||||
end
|
||||
end
|
||||
|
||||
def warn_if_disabled do
|
||||
unless Config.get([:http_security, :enabled]) do
|
||||
Logger.warn("
|
||||
|
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do
|
|||
def init(options), do: options
|
||||
|
||||
def call(conn, _) do
|
||||
if enabled?() and accepts_html?(conn) do
|
||||
if enabled?() and requires_html?(conn) do
|
||||
conn
|
||||
|> StaticFEController.call(:show)
|
||||
|> halt()
|
||||
|
@ -20,10 +20,7 @@ def call(conn, _) do
|
|||
|
||||
defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
|
||||
|
||||
defp accepts_html?(conn) do
|
||||
case get_req_header(conn, "accept") do
|
||||
[accept | _] -> String.contains?(accept, "text/html")
|
||||
_ -> false
|
||||
end
|
||||
defp requires_html?(conn) do
|
||||
Phoenix.Controller.get_format(conn) == "html"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,37 +7,18 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do
|
|||
import Plug.Conn
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do
|
||||
token = assigns[:token]
|
||||
|
||||
cond do
|
||||
not Pleroma.Config.enforce_oauth_admin_scope_usage?() ->
|
||||
conn
|
||||
|
||||
token && OAuth.Scopes.contains_admin_scopes?(token.scopes) ->
|
||||
# Note: checking for _any_ admin scope presence, not necessarily fitting requested action.
|
||||
# Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements.
|
||||
# Admin might opt out of admin scope for some apps to block any admin actions from them.
|
||||
conn
|
||||
|
||||
true ->
|
||||
fail(conn)
|
||||
end
|
||||
def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do
|
||||
conn
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
fail(conn)
|
||||
end
|
||||
|
||||
defp fail(conn) do
|
||||
conn
|
||||
|> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.")
|
||||
|> render_error(:forbidden, "User is not an admin.")
|
||||
|> halt()
|
||||
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
|
|
@ -48,7 +48,7 @@ def stream_body(%{pid: pid, opts: opts, fin: true}) do
|
|||
# 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)
|
||||
if conn, do: :ok = Pleroma.Gun.ConnectionPool.release_conn(conn)
|
||||
|
||||
:done
|
||||
end
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ReverseProxy do
|
||||
@range_headers ~w(range if-range)
|
||||
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
|
||||
~w(if-unmodified-since if-none-match if-range range)
|
||||
~w(if-unmodified-since if-none-match) ++ @range_headers
|
||||
@resp_cache_headers ~w(etag date last-modified)
|
||||
@keep_resp_headers @resp_cache_headers ++
|
||||
~w(content-type content-disposition content-encoding content-range) ++
|
||||
~w(accept-ranges vary)
|
||||
~w(content-length content-type content-disposition content-encoding) ++
|
||||
~w(content-range accept-ranges vary)
|
||||
@default_cache_control_header "public, max-age=1209600"
|
||||
@valid_resp_codes [200, 206, 304]
|
||||
@max_read_duration :timer.seconds(30)
|
||||
|
@ -172,6 +173,8 @@ defp request(method, url, headers, opts) do
|
|||
end
|
||||
|
||||
defp response(conn, client, url, status, headers, opts) do
|
||||
Logger.debug("#{__MODULE__} #{status} #{url} #{inspect(headers)}")
|
||||
|
||||
result =
|
||||
conn
|
||||
|> put_resp_headers(build_resp_headers(headers, opts))
|
||||
|
@ -222,7 +225,9 @@ defp chunk_reply(conn, client, opts, sent_so_far, duration) do
|
|||
end
|
||||
end
|
||||
|
||||
defp head_response(conn, _url, code, headers, opts) do
|
||||
defp head_response(conn, url, code, headers, opts) do
|
||||
Logger.debug("#{__MODULE__} #{code} #{url} #{inspect(headers)}")
|
||||
|
||||
conn
|
||||
|> put_resp_headers(build_resp_headers(headers, opts))
|
||||
|> send_resp(code, "")
|
||||
|
@ -264,20 +269,33 @@ defp build_req_headers(headers, opts) do
|
|||
headers
|
||||
|> downcase_headers()
|
||||
|> Enum.filter(fn {k, _} -> k in @keep_req_headers end)
|
||||
|> (fn headers ->
|
||||
headers = headers ++ Keyword.get(opts, :req_headers, [])
|
||||
|> build_req_range_or_encoding_header(opts)
|
||||
|> build_req_user_agent_header(opts)
|
||||
|> Keyword.merge(Keyword.get(opts, :req_headers, []))
|
||||
end
|
||||
|
||||
if Keyword.get(opts, :keep_user_agent, false) do
|
||||
List.keystore(
|
||||
headers,
|
||||
"user-agent",
|
||||
0,
|
||||
{"user-agent", Pleroma.Application.user_agent()}
|
||||
)
|
||||
else
|
||||
headers
|
||||
end
|
||||
end).()
|
||||
# Disable content-encoding if any @range_headers are requested (see #1823).
|
||||
defp build_req_range_or_encoding_header(headers, _opts) do
|
||||
range? = Enum.any?(headers, fn {header, _} -> Enum.member?(@range_headers, header) end)
|
||||
|
||||
if range? && List.keymember?(headers, "accept-encoding", 0) do
|
||||
List.keydelete(headers, "accept-encoding", 0)
|
||||
else
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
defp build_req_user_agent_header(headers, opts) do
|
||||
if Keyword.get(opts, :keep_user_agent, false) do
|
||||
List.keystore(
|
||||
headers,
|
||||
"user-agent",
|
||||
0,
|
||||
{"user-agent", Pleroma.Application.user_agent()}
|
||||
)
|
||||
else
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
defp build_resp_headers(headers, opts) do
|
||||
|
@ -285,7 +303,7 @@ defp build_resp_headers(headers, opts) do
|
|||
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|
||||
|> build_resp_cache_headers(opts)
|
||||
|> build_resp_content_disposition_header(opts)
|
||||
|> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).()
|
||||
|> Keyword.merge(Keyword.get(opts, :resp_headers, []))
|
||||
end
|
||||
|
||||
defp build_resp_cache_headers(headers, _opts) do
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -63,6 +63,10 @@ def store(upload, opts \\ []) do
|
|||
with {:ok, upload} <- prepare_upload(upload, opts),
|
||||
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
|
||||
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
|
||||
description = Map.get(opts, :description) || upload.name,
|
||||
{_, true} <-
|
||||
{:description_limit,
|
||||
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
|
||||
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
|
||||
{:ok,
|
||||
%{
|
||||
|
@ -75,9 +79,12 @@ def store(upload, opts \\ []) do
|
|||
"href" => url_from_spec(upload, opts.base_url, url_spec)
|
||||
}
|
||||
],
|
||||
"name" => Map.get(opts, :description) || upload.name
|
||||
"name" => description
|
||||
}}
|
||||
else
|
||||
{:description_limit, _} ->
|
||||
{:error, :description_too_long}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error(
|
||||
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.Exiftool do
|
||||
@moduledoc """
|
||||
Strips GPS related EXIF tags and overwrites the file in place.
|
||||
Also strips or replaces filesystem metadata e.g., timestamps.
|
||||
"""
|
||||
@behaviour Pleroma.Upload.Filter
|
||||
|
||||
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
|
||||
System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true)
|
||||
:ok
|
||||
end
|
||||
|
||||
def filter(_), do: :ok
|
||||
end
|
|
@ -89,7 +89,7 @@ defmodule Pleroma.User do
|
|||
field(:keys, :string)
|
||||
field(:public_key, :string)
|
||||
field(:ap_id, :string)
|
||||
field(:avatar, :map)
|
||||
field(:avatar, :map, default: %{})
|
||||
field(:local, :boolean, default: true)
|
||||
field(:follower_address, :string)
|
||||
field(:following_address, :string)
|
||||
|
@ -138,6 +138,7 @@ defmodule Pleroma.User do
|
|||
field(:also_known_as, {:array, :string}, default: [])
|
||||
field(:inbox, :string)
|
||||
field(:shared_inbox, :string)
|
||||
field(:accepts_chat_messages, :boolean, default: nil)
|
||||
|
||||
embeds_one(
|
||||
:notification_settings,
|
||||
|
@ -388,8 +389,8 @@ defp fix_follower_address(%{nickname: nickname} = params),
|
|||
defp fix_follower_address(params), do: params
|
||||
|
||||
def remote_user_changeset(struct \\ %User{local: false}, params) do
|
||||
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
|
||||
bio_limit = Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Config.get([:instance, :user_name_length], 100)
|
||||
|
||||
name =
|
||||
case params[:name] do
|
||||
|
@ -436,7 +437,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|
|||
:discoverable,
|
||||
:invisible,
|
||||
:actor_type,
|
||||
:also_known_as
|
||||
:also_known_as,
|
||||
:accepts_chat_messages
|
||||
]
|
||||
)
|
||||
|> validate_required([:name, :ap_id])
|
||||
|
@ -448,8 +450,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|
|||
end
|
||||
|
||||
def update_changeset(struct, params \\ %{}) do
|
||||
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
|
||||
bio_limit = Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Config.get([:instance, :user_name_length], 100)
|
||||
|
||||
struct
|
||||
|> cast(
|
||||
|
@ -481,7 +483,8 @@ def update_changeset(struct, params \\ %{}) do
|
|||
:pleroma_settings_store,
|
||||
:discoverable,
|
||||
:actor_type,
|
||||
:also_known_as
|
||||
:also_known_as,
|
||||
:accepts_chat_messages
|
||||
]
|
||||
)
|
||||
|> unique_constraint(:nickname)
|
||||
|
@ -527,11 +530,21 @@ defp parse_fields(value) do
|
|||
end
|
||||
|
||||
defp put_emoji(changeset) do
|
||||
bio = get_change(changeset, :bio)
|
||||
name = get_change(changeset, :name)
|
||||
emojified_fields = [:bio, :name, :raw_fields]
|
||||
|
||||
if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
|
||||
bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
|
||||
name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
|
||||
|
||||
emoji = Map.merge(bio, name)
|
||||
|
||||
emoji =
|
||||
changeset
|
||||
|> get_field(:raw_fields)
|
||||
|> Enum.reduce(emoji, fn x, acc ->
|
||||
Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
|
||||
end)
|
||||
|
||||
if bio || name do
|
||||
emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name))
|
||||
put_change(changeset, :emoji, emoji)
|
||||
else
|
||||
changeset
|
||||
|
@ -539,14 +552,11 @@ defp put_emoji(changeset) do
|
|||
end
|
||||
|
||||
defp put_change_if_present(changeset, map_field, value_function) do
|
||||
if value = get_change(changeset, map_field) do
|
||||
with {:ok, new_value} <- value_function.(value) do
|
||||
put_change(changeset, map_field, new_value)
|
||||
else
|
||||
_ -> changeset
|
||||
end
|
||||
with {:ok, value} <- fetch_change(changeset, map_field),
|
||||
{:ok, new_value} <- value_function.(value) do
|
||||
put_change(changeset, map_field, new_value)
|
||||
else
|
||||
changeset
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -621,12 +631,13 @@ def force_password_reset_async(user) do
|
|||
def force_password_reset(user), do: update_password_reset_pending(user, true)
|
||||
|
||||
def register_changeset(struct, params \\ %{}, opts \\ []) do
|
||||
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
|
||||
bio_limit = Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Config.get([:instance, :user_name_length], 100)
|
||||
params = Map.put_new(params, :accepts_chat_messages, true)
|
||||
|
||||
need_confirmation? =
|
||||
if is_nil(opts[:need_confirmation]) do
|
||||
Pleroma.Config.get([:instance, :account_activation_required])
|
||||
Config.get([:instance, :account_activation_required])
|
||||
else
|
||||
opts[:need_confirmation]
|
||||
end
|
||||
|
@ -641,13 +652,14 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|
|||
:nickname,
|
||||
:password,
|
||||
:password_confirmation,
|
||||
:emoji
|
||||
:emoji,
|
||||
:accepts_chat_messages
|
||||
])
|
||||
|> validate_required([:name, :nickname, :password, :password_confirmation])
|
||||
|> validate_confirmation(:password)
|
||||
|> unique_constraint(:email)
|
||||
|> unique_constraint(:nickname)
|
||||
|> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
|
||||
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|
||||
|> validate_format(:nickname, local_nickname_regex())
|
||||
|> validate_format(:email, @email_regex)
|
||||
|> validate_length(:bio, max: bio_limit)
|
||||
|
@ -662,7 +674,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|
|||
def maybe_validate_required_email(changeset, true), do: changeset
|
||||
|
||||
def maybe_validate_required_email(changeset, _) do
|
||||
if Pleroma.Config.get([:instance, :account_activation_required]) do
|
||||
if Config.get([:instance, :account_activation_required]) do
|
||||
validate_required(changeset, [:email])
|
||||
else
|
||||
changeset
|
||||
|
@ -682,7 +694,7 @@ defp put_following_and_follower_address(changeset) do
|
|||
end
|
||||
|
||||
defp autofollow_users(user) do
|
||||
candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
|
||||
candidates = Config.get([:instance, :autofollowed_nicknames])
|
||||
|
||||
autofollowed_users =
|
||||
User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|
||||
|
@ -709,7 +721,7 @@ def post_register_action(%User{} = user) do
|
|||
|
||||
def try_send_confirmation_email(%User{} = user) do
|
||||
if user.confirmation_pending &&
|
||||
Pleroma.Config.get([:instance, :account_activation_required]) do
|
||||
Config.get([:instance, :account_activation_required]) do
|
||||
user
|
||||
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|
||||
|> Pleroma.Emails.Mailer.deliver_async()
|
||||
|
@ -766,7 +778,7 @@ def follow_all(follower, followeds) do
|
|||
defdelegate following(user), to: FollowingRelationship
|
||||
|
||||
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
|
||||
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
|
||||
deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
|
||||
|
||||
cond do
|
||||
followed.deactivated ->
|
||||
|
@ -967,7 +979,7 @@ def get_cached_by_nickname(nickname) do
|
|||
end
|
||||
|
||||
def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
|
||||
restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
|
||||
restrict_to_local = Config.get([:instance, :limit_to_local_content])
|
||||
|
||||
cond do
|
||||
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
|
||||
|
@ -1163,7 +1175,7 @@ defp follow_information_changeset(user, params) do
|
|||
|
||||
@spec update_follower_count(User.t()) :: {:ok, User.t()}
|
||||
def update_follower_count(%User{} = user) do
|
||||
if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
|
||||
if user.local or !Config.get([:instance, :external_user_synchronization]) do
|
||||
follower_count = FollowingRelationship.follower_count(user)
|
||||
|
||||
user
|
||||
|
@ -1176,7 +1188,7 @@ def update_follower_count(%User{} = user) do
|
|||
|
||||
@spec update_following_count(User.t()) :: {:ok, User.t()}
|
||||
def update_following_count(%User{local: false} = user) do
|
||||
if Pleroma.Config.get([:instance, :external_user_synchronization]) do
|
||||
if Config.get([:instance, :external_user_synchronization]) do
|
||||
{:ok, maybe_fetch_follow_information(user)}
|
||||
else
|
||||
{:ok, user}
|
||||
|
@ -1263,7 +1275,7 @@ def unmute(%User{} = muter, %User{} = mutee) do
|
|||
end
|
||||
|
||||
def subscribe(%User{} = subscriber, %User{} = target) do
|
||||
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
|
||||
deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
|
||||
|
||||
if blocks?(target, subscriber) and deny_follow_blocked do
|
||||
{:error, "Could not subscribe: #{target.nickname} is blocking you"}
|
||||
|
@ -1546,7 +1558,7 @@ def perform(:follow_import, %User{} = follower, followed_identifiers)
|
|||
fn followed_identifier ->
|
||||
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
|
||||
{:ok, follower} <- maybe_direct_follow(follower, followed),
|
||||
{:ok, _} <- ActivityPub.follow(follower, followed) do
|
||||
{:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
|
||||
followed
|
||||
else
|
||||
err ->
|
||||
|
@ -1654,7 +1666,7 @@ def html_filter_policy(%User{no_rich_text: true}) do
|
|||
Pleroma.HTML.Scrubber.TwitterText
|
||||
end
|
||||
|
||||
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
|
||||
def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
|
||||
|
||||
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
|
||||
|
||||
|
@ -1836,7 +1848,7 @@ defp normalize_tags(tags) do
|
|||
end
|
||||
|
||||
defp local_nickname_regex do
|
||||
if Pleroma.Config.get([:instance, :extended_nickname_format]) do
|
||||
if Config.get([:instance, :extended_nickname_format]) do
|
||||
@extended_local_nickname_regex
|
||||
else
|
||||
@strict_local_nickname_regex
|
||||
|
@ -1964,8 +1976,8 @@ def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
|
|||
|
||||
def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
|
||||
# use instance-default
|
||||
config = Pleroma.Config.get([:assets, :mascots])
|
||||
default_mascot = Pleroma.Config.get([:assets, :default_mascot])
|
||||
config = Config.get([:assets, :mascots])
|
||||
default_mascot = Config.get([:assets, :default_mascot])
|
||||
mascot = Keyword.get(config, default_mascot)
|
||||
|
||||
%{
|
||||
|
@ -2060,7 +2072,7 @@ def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
|
|||
|
||||
def validate_fields(changeset, remote? \\ false) do
|
||||
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
|
||||
limit = Pleroma.Config.get([:instance, limit_name], 0)
|
||||
limit = Config.get([:instance, limit_name], 0)
|
||||
|
||||
changeset
|
||||
|> validate_length(:fields, max: limit)
|
||||
|
@ -2074,8 +2086,8 @@ def validate_fields(changeset, remote? \\ false) do
|
|||
end
|
||||
|
||||
defp valid_field?(%{"name" => name, "value" => value}) do
|
||||
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
|
||||
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
|
||||
name_limit = Config.get([:instance, :account_field_name_length], 255)
|
||||
value_limit = Config.get([:instance, :account_field_value_length], 255)
|
||||
|
||||
is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
|
||||
String.length(value) <= value_limit
|
||||
|
@ -2085,10 +2097,10 @@ defp valid_field?(_), do: false
|
|||
|
||||
defp truncate_field(%{"name" => name, "value" => value}) do
|
||||
{name, _chopped} =
|
||||
String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
|
||||
String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
|
||||
|
||||
{value, _chopped} =
|
||||
String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
|
||||
String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
|
||||
|
||||
%{"name" => name, "value" => value}
|
||||
end
|
||||
|
@ -2143,7 +2155,7 @@ def confirmation_changeset(user, need_confirmation: need_confirmation?) do
|
|||
|
||||
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
|
||||
if id not in user.pinned_activities do
|
||||
max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
|
||||
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
|
||||
params = %{pinned_activities: user.pinned_activities ++ [id]}
|
||||
|
||||
user
|
||||
|
|
|
@ -10,21 +10,15 @@ defmodule Pleroma.User.NotificationSetting do
|
|||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:followers, :boolean, default: true)
|
||||
field(:follows, :boolean, default: true)
|
||||
field(:non_follows, :boolean, default: true)
|
||||
field(:non_followers, :boolean, default: true)
|
||||
field(:privacy_option, :boolean, default: false)
|
||||
field(:block_from_strangers, :boolean, default: false)
|
||||
field(:hide_notification_contents, :boolean, default: false)
|
||||
end
|
||||
|
||||
def changeset(schema, params) do
|
||||
schema
|
||||
|> cast(prepare_attrs(params), [
|
||||
:followers,
|
||||
:follows,
|
||||
:non_follows,
|
||||
:non_followers,
|
||||
:privacy_option
|
||||
:block_from_strangers,
|
||||
:hide_notification_contents
|
||||
])
|
||||
end
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ defp search_query(query_string, for_user, following) do
|
|||
|> base_query(following)
|
||||
|> filter_blocked_user(for_user)
|
||||
|> filter_invisible_users()
|
||||
|> filter_internal_users()
|
||||
|> filter_blocked_domains(for_user)
|
||||
|> fts_search(query_string)
|
||||
|> trigram_rank(query_string)
|
||||
|
@ -68,11 +69,15 @@ defp fts_search(query, query_string) do
|
|||
u in query,
|
||||
where:
|
||||
fragment(
|
||||
# The fragment must _exactly_ match `users_fts_index`, otherwise the index won't work
|
||||
"""
|
||||
(to_tsvector('simple', ?) || to_tsvector('simple', ?)) @@ to_tsquery('simple', ?)
|
||||
(
|
||||
setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
|
||||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')
|
||||
) @@ to_tsquery('simple', ?)
|
||||
""",
|
||||
u.name,
|
||||
u.nickname,
|
||||
u.name,
|
||||
^query_string
|
||||
)
|
||||
)
|
||||
|
@ -87,15 +92,23 @@ defp to_tsquery(query_string) do
|
|||
|> Enum.join(" | ")
|
||||
end
|
||||
|
||||
# Considers nickname match, localized nickname match, name match; preferences nickname match
|
||||
defp trigram_rank(query, query_string) do
|
||||
from(
|
||||
u in query,
|
||||
select_merge: %{
|
||||
search_rank:
|
||||
fragment(
|
||||
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
|
||||
"""
|
||||
similarity(?, ?) +
|
||||
similarity(?, regexp_replace(?, '@.+', '')) +
|
||||
similarity(?, trim(coalesce(?, '')))
|
||||
""",
|
||||
^query_string,
|
||||
u.nickname,
|
||||
^query_string,
|
||||
u.nickname,
|
||||
^query_string,
|
||||
u.name
|
||||
)
|
||||
}
|
||||
|
@ -109,6 +122,10 @@ defp filter_invisible_users(query) do
|
|||
from(q in query, where: q.invisible == false)
|
||||
end
|
||||
|
||||
defp filter_internal_users(query) do
|
||||
from(q in query, where: q.actor_type != "Application")
|
||||
end
|
||||
|
||||
defp filter_blocked_user(query, %User{} = blocker) do
|
||||
query
|
||||
|> join(:left, [u], b in Pleroma.UserRelationship,
|
||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
alias Pleroma.Constants
|
||||
alias Pleroma.Conversation
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Filter
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
|
@ -321,28 +322,6 @@ defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) ::
|
||||
{:ok, Activity.t()} | {:error, any()}
|
||||
def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do
|
||||
with {:ok, result} <-
|
||||
Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp do_follow(follower, followed, activity_id, local, opts) do
|
||||
skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false)
|
||||
data = make_follow_data(follower, followed, activity_id)
|
||||
|
||||
with {:ok, activity} <- insert(data, local),
|
||||
_ <- skip_notify_and_stream || notify_and_stream(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, error} -> Repo.rollback(error)
|
||||
end
|
||||
end
|
||||
|
||||
@spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
|
||||
{:ok, Activity.t()} | nil | {:error, any()}
|
||||
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
|
||||
|
@ -446,6 +425,7 @@ def fetch_activities_for_context_query(context, opts) do
|
|||
|> maybe_set_thread_muted_field(opts)
|
||||
|> restrict_blocked(opts)
|
||||
|> restrict_recipients(recipients, opts[:user])
|
||||
|> restrict_filtered(opts)
|
||||
|> where(
|
||||
[activity],
|
||||
fragment(
|
||||
|
@ -961,6 +941,26 @@ defp restrict_instance(query, %{instance: instance}) do
|
|||
|
||||
defp restrict_instance(query, _), do: query
|
||||
|
||||
defp restrict_filtered(query, %{user: %User{} = user}) do
|
||||
case Filter.compose_regex(user) do
|
||||
nil ->
|
||||
query
|
||||
|
||||
regex ->
|
||||
from([activity, object] in query,
|
||||
where:
|
||||
fragment("not(?->>'content' ~* ?)", object.data, ^regex) or
|
||||
activity.actor == ^user.ap_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_filtered(query, %{blocking_user: %User{} = user}) do
|
||||
restrict_filtered(query, %{user: user})
|
||||
end
|
||||
|
||||
defp restrict_filtered(query, _), do: query
|
||||
|
||||
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
|
||||
|
||||
defp exclude_poll_votes(query, _) do
|
||||
|
@ -1091,6 +1091,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
|||
|> restrict_favorited_by(opts)
|
||||
|> restrict_blocked(restrict_blocked_opts)
|
||||
|> restrict_muted(restrict_muted_opts)
|
||||
|> restrict_filtered(opts)
|
||||
|> restrict_media(opts)
|
||||
|> restrict_visibility(opts)
|
||||
|> restrict_thread_visibility(opts, config)
|
||||
|
@ -1099,6 +1100,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
|||
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|
||||
|> restrict_instance(opts)
|
||||
|> restrict_announce_object_actor(opts)
|
||||
|> restrict_filtered(opts)
|
||||
|> Activity.restrict_deactivated_users()
|
||||
|> exclude_poll_votes(opts)
|
||||
|> exclude_chat_messages(opts)
|
||||
|
@ -1224,6 +1226,8 @@ defp object_to_user_data(data) do
|
|||
end)
|
||||
|
||||
locked = data["manuallyApprovesFollowers"] || false
|
||||
capabilities = data["capabilities"] || %{}
|
||||
accepts_chat_messages = capabilities["acceptsChatMessages"]
|
||||
data = Transmogrifier.maybe_fix_user_object(data)
|
||||
discoverable = data["discoverable"] || false
|
||||
invisible = data["invisible"] || false
|
||||
|
@ -1262,7 +1266,8 @@ defp object_to_user_data(data) do
|
|||
also_known_as: Map.get(data, "alsoKnownAs", []),
|
||||
public_key: public_key,
|
||||
inbox: data["inbox"],
|
||||
shared_inbox: shared_inbox
|
||||
shared_inbox: shared_inbox,
|
||||
accepts_chat_messages: accepts_chat_messages
|
||||
}
|
||||
|
||||
# nickname can be nil because of virtual actors
|
||||
|
@ -1371,13 +1376,28 @@ def fetch_and_prepare_user_from_ap_id(ap_id) do
|
|||
end
|
||||
end
|
||||
|
||||
def maybe_handle_clashing_nickname(nickname) do
|
||||
with %User{} = old_user <- User.get_by_nickname(nickname) do
|
||||
Logger.info("Found an old user for #{nickname}, ap id is #{old_user.ap_id}, renaming.")
|
||||
def maybe_handle_clashing_nickname(data) do
|
||||
nickname = data[:nickname]
|
||||
|
||||
with %User{} = old_user <- User.get_by_nickname(nickname),
|
||||
{_, false} <- {:ap_id_comparison, data[:ap_id] == old_user.ap_id} do
|
||||
Logger.info(
|
||||
"Found an old user for #{nickname}, the old ap id is #{old_user.ap_id}, new one is #{
|
||||
data[:ap_id]
|
||||
}, renaming."
|
||||
)
|
||||
|
||||
old_user
|
||||
|> User.remote_user_changeset(%{nickname: "#{old_user.id}.#{old_user.nickname}"})
|
||||
|> User.update_and_set_cache()
|
||||
else
|
||||
{:ap_id_comparison, true} ->
|
||||
Logger.info(
|
||||
"Found an old user for #{nickname}, but the ap id #{data[:ap_id]} is the same as the new user. Race condition? Not changing anything."
|
||||
)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1393,7 +1413,7 @@ def make_user_from_ap_id(ap_id) do
|
|||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
else
|
||||
maybe_handle_clashing_nickname(data[:nickname])
|
||||
maybe_handle_clashing_nickname(data)
|
||||
|
||||
data
|
||||
|> User.remote_user_changeset()
|
||||
|
|
|
@ -14,6 +14,19 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
|
||||
require Pleroma.Constants
|
||||
|
||||
@spec follow(User.t(), User.t()) :: {:ok, map(), keyword()}
|
||||
def follow(follower, followed) do
|
||||
data = %{
|
||||
"id" => Utils.generate_activity_id(),
|
||||
"actor" => follower.ap_id,
|
||||
"type" => "Follow",
|
||||
"object" => followed.ap_id,
|
||||
"to" => [followed.ap_id]
|
||||
}
|
||||
|
||||
{:ok, data, []}
|
||||
end
|
||||
|
||||
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
|
||||
def emoji_react(actor, object, emoji) do
|
||||
with {:ok, data, meta} <- object_action(actor, object) do
|
||||
|
|
|
@ -60,7 +60,7 @@ def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
|
|||
if score < 0.8 do
|
||||
{:ok, message}
|
||||
else
|
||||
{:reject, nil}
|
||||
{:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -39,14 +39,13 @@ def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message
|
|||
{:ok, message}
|
||||
|
||||
{:old_user, false} ->
|
||||
{:reject, nil}
|
||||
{:reject, "[AntiLinkSpamPolicy] User has no posts nor followers"}
|
||||
|
||||
{:error, _} ->
|
||||
{:reject, nil}
|
||||
{:reject, "[AntiLinkSpamPolicy] Failed to get or fetch user by ap_id"}
|
||||
|
||||
e ->
|
||||
Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}")
|
||||
{:reject, nil}
|
||||
{:reject, "[AntiLinkSpamPolicy] Unhandled error #{inspect(e)}"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ defp delist_message(message, _threshold), do: {:ok, message}
|
|||
defp reject_message(message, threshold) when threshold > 0 do
|
||||
with {_, recipients} <- get_recipient_count(message) do
|
||||
if recipients > threshold do
|
||||
{:reject, nil}
|
||||
{:reject, "[HellthreadPolicy] #{recipients} recipients is over the limit of #{threshold}"}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
|
@ -87,7 +87,7 @@ def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = message
|
|||
{:ok, message} <- delist_message(message, delist_threshold) do
|
||||
{:ok, message}
|
||||
else
|
||||
_e -> {:reject, nil}
|
||||
e -> e
|
||||
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 ->
|
||||
string_matches?(content, pattern) or string_matches?(summary, pattern)
|
||||
end) do
|
||||
{:reject, nil}
|
||||
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
|
@ -89,8 +89,9 @@ def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message
|
|||
{:ok, message} <- check_replace(message) do
|
||||
{:ok, message}
|
||||
else
|
||||
_e ->
|
||||
{:reject, nil}
|
||||
{:reject, nil} -> {:reject, "[KeywordPolicy] "}
|
||||
{:reject, _} = e -> e
|
||||
_e -> {:reject, "[KeywordPolicy] "}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -12,8 +12,9 @@ def filter(%{"type" => "Create"} = message) do
|
|||
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])
|
||||
recipients = (message["to"] || []) ++ (message["cc"] || [])
|
||||
|
||||
if Enum.any?(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
|
||||
{:reject, nil}
|
||||
if rejected_mention =
|
||||
Enum.find(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
|
||||
{:reject, "[MentionPolicy] Rejected for mention of #{rejected_mention}"}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
|
|
|
@ -28,7 +28,7 @@ defp check_date(%{"object" => %{"published" => published}} = message) do
|
|||
|
||||
defp check_reject(message, actions) do
|
||||
if :reject in actions do
|
||||
{:reject, nil}
|
||||
{:reject, "[ObjectAgePolicy]"}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
|
@ -47,9 +47,8 @@ defp check_delist(message, actions) do
|
|||
|
||||
{:ok, message}
|
||||
else
|
||||
# Unhandleable error: somebody is messing around, just drop the message.
|
||||
_e ->
|
||||
{:reject, nil}
|
||||
{:reject, "[ObjectAgePolicy] Unhandled error"}
|
||||
end
|
||||
else
|
||||
{:ok, message}
|
||||
|
@ -69,9 +68,8 @@ defp check_strip_followers(message, actions) do
|
|||
|
||||
{:ok, message}
|
||||
else
|
||||
# Unhandleable error: somebody is messing around, just drop the message.
|
||||
_e ->
|
||||
{:reject, nil}
|
||||
{:reject, "[ObjectAgePolicy] Unhandled error"}
|
||||
end
|
||||
else
|
||||
{:ok, message}
|
||||
|
@ -98,7 +96,7 @@ def filter(message), do: {:ok, message}
|
|||
@impl true
|
||||
def describe do
|
||||
mrf_object_age =
|
||||
Pleroma.Config.get(:mrf_object_age)
|
||||
Config.get(:mrf_object_age)
|
||||
|> Enum.into(%{})
|
||||
|
||||
{:ok, %{mrf_object_age: mrf_object_age}}
|
||||
|
|
|
@ -38,7 +38,7 @@ def filter(%{"type" => "Create"} = object) do
|
|||
{:ok, object}
|
||||
|
||||
true ->
|
||||
{:reject, nil}
|
||||
{:reject, "[RejectNonPublic] visibility: #{visibility}"}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -47,5 +47,5 @@ def filter(object), do: {:ok, object}
|
|||
|
||||
@impl true
|
||||
def describe,
|
||||
do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
|
||||
do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ defp check_accept(%{host: actor_host} = _actor_info, object) do
|
|||
accepts == [] -> {:ok, object}
|
||||
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
|
||||
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
|
||||
true -> {:reject, nil}
|
||||
true -> {:reject, "[SimplePolicy] host not in accept list"}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -31,7 +31,7 @@ defp check_reject(%{host: actor_host} = _actor_info, object) do
|
|||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(rejects, actor_host) do
|
||||
{:reject, nil}
|
||||
{:reject, "[SimplePolicy] host in reject list"}
|
||||
else
|
||||
{:ok, object}
|
||||
end
|
||||
|
@ -114,7 +114,7 @@ defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"}
|
|||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(report_removal, actor_host) do
|
||||
{:reject, nil}
|
||||
{:reject, "[SimplePolicy] host in report_removal list"}
|
||||
else
|
||||
{:ok, object}
|
||||
end
|
||||
|
@ -155,11 +155,11 @@ def filter(%{"type" => "Delete", "actor" => actor} = object) do
|
|||
%{host: actor_host} = URI.parse(actor)
|
||||
|
||||
reject_deletes =
|
||||
Pleroma.Config.get([:mrf_simple, :reject_deletes])
|
||||
Config.get([:mrf_simple, :reject_deletes])
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(reject_deletes, actor_host) do
|
||||
{:reject, nil}
|
||||
{:reject, "[SimplePolicy] host in reject_deletes list"}
|
||||
else
|
||||
{:ok, object}
|
||||
end
|
||||
|
@ -177,7 +177,9 @@ def filter(%{"actor" => actor} = object) do
|
|||
{:ok, object} <- check_report_removal(actor_info, object) do
|
||||
{:ok, object}
|
||||
else
|
||||
_e -> {:reject, nil}
|
||||
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||
{:reject, _} = e -> e
|
||||
_ -> {:reject, "[SimplePolicy]"}
|
||||
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}
|
||||
else
|
||||
_e -> {:reject, nil}
|
||||
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||
{:reject, _} = e -> e
|
||||
_ -> {:reject, "[SimplePolicy]"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -134,12 +134,13 @@ defp process_tag(
|
|||
if user.local == true do
|
||||
{:ok, message}
|
||||
else
|
||||
{:reject, nil}
|
||||
{:reject,
|
||||
"[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-remote-subscription"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}),
|
||||
do: {:reject, nil}
|
||||
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow", "actor" => actor}),
|
||||
do: {:reject, "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-any-subscription"}
|
||||
|
||||
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
|
||||
{:ok, object}
|
||||
else
|
||||
{:reject, nil}
|
||||
{:reject, "[UserAllowListPolicy] #{actor} not in the list"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -11,22 +11,26 @@ def filter(%{"type" => "Undo", "object" => child_message} = message) do
|
|||
with {:ok, _} <- filter(child_message) do
|
||||
{:ok, message}
|
||||
else
|
||||
{:reject, nil} ->
|
||||
{:reject, nil}
|
||||
{:reject, _} = e -> e
|
||||
end
|
||||
end
|
||||
|
||||
def filter(%{"type" => message_type} = message) do
|
||||
with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]),
|
||||
rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]),
|
||||
true <-
|
||||
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type),
|
||||
false <-
|
||||
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type),
|
||||
{_, true} <-
|
||||
{:accepted,
|
||||
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type)},
|
||||
{_, false} <-
|
||||
{:rejected,
|
||||
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type)},
|
||||
{:ok, _} <- filter(message["object"]) do
|
||||
{:ok, message}
|
||||
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
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
|
||||
|
@ -25,6 +26,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
|
||||
def validate(object, meta)
|
||||
|
||||
def validate(%{"type" => "Follow"} = object, meta) do
|
||||
with {:ok, object} <-
|
||||
object
|
||||
|> FollowValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object)
|
||||
{:ok, object, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "Block"} = block_activity, meta) do
|
||||
with {:ok, block_activity} <-
|
||||
block_activity
|
||||
|
|
|
@ -93,12 +93,14 @@ def validate_content_or_attachment(cng) do
|
|||
- If both users are in our system
|
||||
- If at least one of the users in this ChatMessage is a local user
|
||||
- If the recipient is not blocking the actor
|
||||
- If the recipient is explicitly not accepting chat messages
|
||||
"""
|
||||
def validate_local_concern(cng) do
|
||||
with actor_ap <- get_field(cng, :actor),
|
||||
{_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)},
|
||||
{_, %User{} = recipient} <-
|
||||
{:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())},
|
||||
{_, false} <- {:not_accepting_chats?, recipient.accepts_chat_messages == false},
|
||||
{_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)},
|
||||
{_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do
|
||||
cng
|
||||
|
@ -107,6 +109,10 @@ def validate_local_concern(cng) do
|
|||
cng
|
||||
|> add_error(:actor, "actor is blocked by recipient")
|
||||
|
||||
{:not_accepting_chats?, true} ->
|
||||
cng
|
||||
|> add_error(:to, "recipient does not accept chat messages")
|
||||
|
||||
{:local?, false} ->
|
||||
cng
|
||||
|> add_error(:actor, "actor and recipient are both remote")
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, ObjectValidators.ObjectID, primary_key: true)
|
||||
field(:type, :string)
|
||||
field(:actor, ObjectValidators.ObjectID)
|
||||
field(:to, ObjectValidators.Recipients, default: [])
|
||||
field(:cc, ObjectValidators.Recipients, default: [])
|
||||
field(:object, ObjectValidators.ObjectID)
|
||||
field(:state, :string, default: "pending")
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> cast(data, __schema__(:fields))
|
||||
end
|
||||
|
||||
def validate_data(cng) do
|
||||
cng
|
||||
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||
|> validate_inclusion(:type, ["Follow"])
|
||||
|> validate_inclusion(:state, ~w{pending reject accept})
|
||||
|> validate_actor_presence()
|
||||
|> validate_actor_presence(field_name: :object)
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data
|
||||
|> validate_data
|
||||
end
|
||||
end
|
|
@ -49,7 +49,8 @@ def is_representable?(%Activity{} = activity) do
|
|||
"""
|
||||
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
|
||||
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())
|
||||
|
||||
|
@ -57,8 +58,8 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
|
|||
|
||||
signature =
|
||||
Pleroma.Signature.sign(actor, %{
|
||||
"(request-target)": "post #{path}",
|
||||
host: host,
|
||||
"(request-target)": "post #{uri.path}",
|
||||
host: signature_host(uri),
|
||||
"content-length": byte_size(json),
|
||||
digest: digest,
|
||||
date: date
|
||||
|
@ -76,8 +77,9 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
|
|||
{"digest", digest}
|
||||
]
|
||||
) do
|
||||
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
|
||||
do: Instances.set_reachable(inbox)
|
||||
if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
|
||||
Instances.set_reachable(inbox)
|
||||
end
|
||||
|
||||
result
|
||||
else
|
||||
|
@ -96,6 +98,14 @@ def publish_one(%{actor_id: actor_id} = params) do
|
|||
|> publish_one()
|
||||
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
|
||||
if public do
|
||||
true
|
||||
|
|
|
@ -28,7 +28,7 @@ def relay_ap_id do
|
|||
def follow(target_instance) do
|
||||
with %User{} = local_user <- get_actor(),
|
||||
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
|
||||
{:ok, activity} <- ActivityPub.follow(local_user, target_user) do
|
||||
{:ok, _, _, activity} <- CommonAPI.follow(local_user, target_user) do
|
||||
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
|
||||
{:ok, activity}
|
||||
else
|
||||
|
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
alias Pleroma.Activity.Ir.Topics
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
@ -21,6 +22,69 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
|
||||
def handle(object, meta \\ [])
|
||||
|
||||
# Tasks this handle
|
||||
# - Follows if possible
|
||||
# - Sends a notification
|
||||
# - Generates accept or reject if appropriate
|
||||
def handle(
|
||||
%{
|
||||
data: %{
|
||||
"id" => follow_id,
|
||||
"type" => "Follow",
|
||||
"object" => followed_user,
|
||||
"actor" => following_user
|
||||
}
|
||||
} = object,
|
||||
meta
|
||||
) do
|
||||
with %User{} = follower <- User.get_cached_by_ap_id(following_user),
|
||||
%User{} = followed <- User.get_cached_by_ap_id(followed_user),
|
||||
{_, {:ok, _}, _, _} <-
|
||||
{:following, User.follow(follower, followed, :follow_pending), follower, followed} do
|
||||
if followed.local && !followed.locked do
|
||||
Utils.update_follow_state_for_all(object, "accept")
|
||||
FollowingRelationship.update(follower, followed, :follow_accept)
|
||||
User.update_follower_count(followed)
|
||||
User.update_following_count(follower)
|
||||
|
||||
%{
|
||||
to: [following_user],
|
||||
actor: followed,
|
||||
object: follow_id,
|
||||
local: true
|
||||
}
|
||||
|> ActivityPub.accept()
|
||||
end
|
||||
else
|
||||
{:following, {:error, _}, follower, followed} ->
|
||||
Utils.update_follow_state_for_all(object, "reject")
|
||||
FollowingRelationship.update(follower, followed, :follow_reject)
|
||||
|
||||
if followed.local do
|
||||
%{
|
||||
to: [follower.ap_id],
|
||||
actor: followed,
|
||||
object: follow_id,
|
||||
local: true
|
||||
}
|
||||
|> ActivityPub.reject()
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
{:ok, notifications} = Notification.create_notifications(object, do_send: false)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_notifications(notifications)
|
||||
|
||||
updated_object = Activity.get_by_ap_id(follow_id)
|
||||
|
||||
{:ok, updated_object, meta}
|
||||
end
|
||||
|
||||
# Tasks this handles:
|
||||
# - Unfollow and block
|
||||
def handle(
|
||||
|
@ -209,14 +273,20 @@ def handle_object_creation(object) do
|
|||
{:ok, object}
|
||||
end
|
||||
|
||||
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
|
||||
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
|
||||
{:ok, _} <- Utils.remove_like_from_object(object, liked_object),
|
||||
{:ok, _} <- Repo.delete(object) do
|
||||
:ok
|
||||
defp undo_like(nil, object), do: delete_object(object)
|
||||
|
||||
defp undo_like(%Object{} = liked_object, object) do
|
||||
with {:ok, _} <- Utils.remove_like_from_object(object, liked_object) do
|
||||
delete_object(object)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
|
||||
object.data["object"]
|
||||
|> Object.get_by_ap_id()
|
||||
|> undo_like(object)
|
||||
end
|
||||
|
||||
def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
|
||||
with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
|
||||
{:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object),
|
||||
|
@ -246,6 +316,11 @@ def handle_undoing(
|
|||
|
||||
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
|
||||
|
||||
@spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()}
|
||||
defp delete_object(object) do
|
||||
with {:ok, _} <- Repo.delete(object), do: :ok
|
||||
end
|
||||
|
||||
defp send_notifications(meta) do
|
||||
Keyword.get(meta, :notifications, [])
|
||||
|> Enum.each(fn notification ->
|
||||
|
|
|
@ -62,15 +62,17 @@ def fix_summary(%{"summary" => _} = object) do
|
|||
def fix_summary(object), do: Map.put(object, "summary", "")
|
||||
|
||||
def fix_addressing_list(map, field) do
|
||||
cond do
|
||||
is_binary(map[field]) ->
|
||||
Map.put(map, field, [map[field]])
|
||||
addrs = map[field]
|
||||
|
||||
is_nil(map[field]) ->
|
||||
Map.put(map, field, [])
|
||||
cond do
|
||||
is_list(addrs) ->
|
||||
Map.put(map, field, Enum.filter(addrs, &is_binary/1))
|
||||
|
||||
is_binary(addrs) ->
|
||||
Map.put(map, field, [addrs])
|
||||
|
||||
true ->
|
||||
map
|
||||
Map.put(map, field, [])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -233,18 +235,24 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
|
|||
is_map(url) && is_binary(url["href"]) -> url["href"]
|
||||
is_binary(data["url"]) -> data["url"]
|
||||
is_binary(data["href"]) -> data["href"]
|
||||
true -> nil
|
||||
end
|
||||
|
||||
attachment_url =
|
||||
%{"href" => href}
|
||||
|> Maps.put_if_present("mediaType", media_type)
|
||||
|> Maps.put_if_present("type", Map.get(url || %{}, "type"))
|
||||
if href do
|
||||
attachment_url =
|
||||
%{"href" => href}
|
||||
|> Maps.put_if_present("mediaType", media_type)
|
||||
|> Maps.put_if_present("type", Map.get(url || %{}, "type"))
|
||||
|
||||
%{"url" => [attachment_url]}
|
||||
|> Maps.put_if_present("mediaType", media_type)
|
||||
|> Maps.put_if_present("type", data["type"])
|
||||
|> Maps.put_if_present("name", data["name"])
|
||||
%{"url" => [attachment_url]}
|
||||
|> Maps.put_if_present("mediaType", media_type)
|
||||
|> Maps.put_if_present("type", data["type"])
|
||||
|> Maps.put_if_present("name", data["name"])
|
||||
else
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
Map.put(object, "attachment", attachments)
|
||||
end
|
||||
|
@ -263,12 +271,18 @@ def fix_url(%{"url" => url} = object) when is_map(url) do
|
|||
|
||||
def fix_url(%{"type" => object_type, "url" => url} = object)
|
||||
when object_type in ["Video", "Audio"] and is_list(url) do
|
||||
first_element = Enum.at(url, 0)
|
||||
attachment =
|
||||
Enum.find(url, fn x ->
|
||||
media_type = x["mediaType"] || x["mimeType"] || ""
|
||||
|
||||
link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
|
||||
is_map(x) and String.starts_with?(media_type, ["audio/", "video/"])
|
||||
end)
|
||||
|
||||
link_element =
|
||||
Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)
|
||||
|
||||
object
|
||||
|> Map.put("attachment", [first_element])
|
||||
|> Map.put("attachment", [attachment])
|
||||
|> Map.put("url", link_element["href"])
|
||||
end
|
||||
|
||||
|
@ -517,66 +531,6 @@ def handle_incoming(
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
|
||||
_options
|
||||
) do
|
||||
with %User{local: true} = followed <-
|
||||
User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
|
||||
{:ok, %User{} = follower} <-
|
||||
User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
|
||||
{:ok, activity} <-
|
||||
ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do
|
||||
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
|
||||
{_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
|
||||
{_, false} <- {:user_locked, User.locked?(followed)},
|
||||
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
|
||||
{_, {:ok, _}} <-
|
||||
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
|
||||
{:ok, _relationship} <-
|
||||
FollowingRelationship.update(follower, followed, :follow_accept) do
|
||||
ActivityPub.accept(%{
|
||||
to: [follower.ap_id],
|
||||
actor: followed,
|
||||
object: data,
|
||||
local: true
|
||||
})
|
||||
else
|
||||
{:user_blocked, true} ->
|
||||
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
|
||||
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
|
||||
|
||||
ActivityPub.reject(%{
|
||||
to: [follower.ap_id],
|
||||
actor: followed,
|
||||
object: data,
|
||||
local: true
|
||||
})
|
||||
|
||||
{:follow, {:error, _}} ->
|
||||
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
|
||||
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
|
||||
|
||||
ActivityPub.reject(%{
|
||||
to: [follower.ap_id],
|
||||
actor: followed,
|
||||
object: data,
|
||||
local: true
|
||||
})
|
||||
|
||||
{:user_locked, true} ->
|
||||
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
|
||||
:noop
|
||||
end
|
||||
|
||||
ActivityPub.notify_and_stream(activity)
|
||||
{:ok, activity}
|
||||
else
|
||||
_e ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
|
||||
_options
|
||||
|
@ -684,7 +638,7 @@ def handle_incoming(
|
|||
%{"type" => type} = data,
|
||||
_options
|
||||
)
|
||||
when type in ~w{Update Block} do
|
||||
when type in ~w{Update Block Follow} do
|
||||
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
|
|
|
@ -81,6 +81,15 @@ def render("user.json", %{user: user}) do
|
|||
|
||||
fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
|
||||
|
||||
capabilities =
|
||||
if is_boolean(user.accepts_chat_messages) do
|
||||
%{
|
||||
"acceptsChatMessages" => user.accepts_chat_messages
|
||||
}
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
||||
%{
|
||||
"id" => user.ap_id,
|
||||
"type" => user.actor_type,
|
||||
|
@ -101,7 +110,8 @@ def render("user.json", %{user: user}) do
|
|||
"endpoints" => endpoints,
|
||||
"attachment" => fields,
|
||||
"tag" => emoji_tags,
|
||||
"discoverable" => user.discoverable
|
||||
"discoverable" => user.discoverable,
|
||||
"capabilities" => capabilities
|
||||
}
|
||||
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|
||||
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
|
||||
|
|
|
@ -206,8 +206,8 @@ def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
|
|||
end
|
||||
end
|
||||
|
||||
def user_show(conn, %{"nickname" => nickname}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
|
||||
def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("show.json", %{user: user})
|
||||
|
@ -233,11 +233,11 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do
|
|||
|> render("index.json", %{activities: activities, as: :activity})
|
||||
end
|
||||
|
||||
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
|
||||
def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do
|
||||
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
|
||||
godmode = params["godmode"] == "true" || params["godmode"] == true
|
||||
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
|
||||
{_, page_size} = page_params(params)
|
||||
|
||||
activities =
|
||||
|
@ -526,7 +526,7 @@ def disable_mfa(conn, %{"nickname" => nickname}) do
|
|||
|
||||
@doc "Show a given user's credentials"
|
||||
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("credentials.json", %{user: user, for: admin})
|
||||
|
|
|
@ -9,8 +9,6 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
alias Pleroma.ConfigDB
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
|
||||
@descriptions Pleroma.Docs.JSON.compile()
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update)
|
||||
|
||||
|
@ -25,7 +23,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
|
|||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation
|
||||
|
||||
def descriptions(conn, _params) do
|
||||
descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
|
||||
descriptions = Enum.filter(Pleroma.Docs.JSON.compiled_descriptions(), &whitelisted_config?/1)
|
||||
|
||||
json(conn, descriptions)
|
||||
end
|
||||
|
|
|
@ -29,6 +29,10 @@ def request_body(description, schema_ref, opts \\ []) do
|
|||
}
|
||||
end
|
||||
|
||||
def admin_api_params do
|
||||
[Operation.parameter(:admin_token, :query, :string, "Allows authorization via admin token.")]
|
||||
end
|
||||
|
||||
def pagination_params do
|
||||
[
|
||||
Operation.parameter(:max_id, :query, :string, "Return items older than this ID"),
|
||||
|
|
|
@ -61,7 +61,7 @@ def update_credentials_operation do
|
|||
description: "Update the user's display and preferences.",
|
||||
operationId: "AccountController.update_credentials",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
requestBody: request_body("Parameters", update_creadentials_request(), required: true),
|
||||
requestBody: request_body("Parameters", update_credentials_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Account", "application/json", Account),
|
||||
403 => Operation.response("Error", "application/json", ApiError)
|
||||
|
@ -159,6 +159,7 @@ def followers_operation do
|
|||
"Accounts which follow the given account, if network is not hidden by the account owner.",
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||
Operation.parameter(:id, :query, :string, "ID of the resource owner"),
|
||||
with_relationships_param() | pagination_params()
|
||||
],
|
||||
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.",
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||
Operation.parameter(:id, :query, :string, "ID of the resource owner"),
|
||||
with_relationships_param() | pagination_params()
|
||||
],
|
||||
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
|
||||
|
@ -203,14 +205,23 @@ def follow_operation do
|
|||
security: [%{"oAuth" => ["follow", "write:follows"]}],
|
||||
description: "Follow the given account",
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
|
||||
Operation.parameter(
|
||||
:reblogs,
|
||||
:query,
|
||||
BooleanLike,
|
||||
"Receive this account's reblogs in home timeline? Defaults to true."
|
||||
)
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
|
||||
],
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
reblogs: %Schema{
|
||||
type: :boolean,
|
||||
description: "Receive this account's reblogs in home timeline? Defaults to true.",
|
||||
default: true
|
||||
}
|
||||
}
|
||||
},
|
||||
required: false
|
||||
),
|
||||
responses: %{
|
||||
200 => Operation.response("Relationship", "application/json", AccountRelationship),
|
||||
400 => Operation.response("Error", "application/json", ApiError),
|
||||
|
@ -438,6 +449,7 @@ defp create_request do
|
|||
}
|
||||
end
|
||||
|
||||
# TODO: This is actually a token respone, but there's no oauth operation file yet.
|
||||
defp create_response do
|
||||
%Schema{
|
||||
title: "AccountCreateResponse",
|
||||
|
@ -446,19 +458,25 @@ defp create_response do
|
|||
properties: %{
|
||||
token_type: %Schema{type: :string},
|
||||
access_token: %Schema{type: :string},
|
||||
scope: %Schema{type: :array, items: %Schema{type: :string}},
|
||||
created_at: %Schema{type: :integer, format: :"date-time"}
|
||||
refresh_token: %Schema{type: :string},
|
||||
scope: %Schema{type: :string},
|
||||
created_at: %Schema{type: :integer, format: :"date-time"},
|
||||
me: %Schema{type: :string},
|
||||
expires_in: %Schema{type: :integer}
|
||||
},
|
||||
example: %{
|
||||
"token_type" => "Bearer",
|
||||
"access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
|
||||
"refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz",
|
||||
"created_at" => 1_585_918_714,
|
||||
"scope" => ["read", "write", "follow", "push"],
|
||||
"token_type" => "Bearer"
|
||||
"expires_in" => 600,
|
||||
"scope" => "read write follow push",
|
||||
"me" => "https://gensokyo.2hu/users/raymoo"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_creadentials_request do
|
||||
defp update_credentials_request do
|
||||
%Schema{
|
||||
title: "AccountUpdateCredentialsRequest",
|
||||
description: "POST body for creating an account",
|
||||
|
@ -492,6 +510,11 @@ defp update_creadentials_request do
|
|||
nullable: true,
|
||||
description: "Whether manual approval of follow requests is required."
|
||||
},
|
||||
accepts_chat_messages: %Schema{
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Whether the user accepts receiving chat messages."
|
||||
},
|
||||
fields_attributes: %Schema{
|
||||
nullable: true,
|
||||
oneOf: [
|
||||
|
|
|
@ -26,6 +26,7 @@ def show_operation do
|
|||
%Schema{type: :boolean, default: false},
|
||||
"Get only saved in database settings"
|
||||
)
|
||||
| admin_api_params()
|
||||
],
|
||||
security: [%{"oAuth" => ["read"]}],
|
||||
responses: %{
|
||||
|
@ -41,6 +42,7 @@ def update_operation do
|
|||
summary: "Update config settings",
|
||||
operationId: "AdminAPI.ConfigController.update",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
type: :object,
|
||||
|
@ -73,6 +75,7 @@ def descriptions_operation do
|
|||
summary: "Get JSON with config descriptions.",
|
||||
operationId: "AdminAPI.ConfigController.descriptions",
|
||||
security: [%{"oAuth" => ["read"]}],
|
||||
parameters: admin_api_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Config Descriptions", "application/json", %Schema{
|
||||
|
|
|
@ -20,6 +20,7 @@ def index_operation do
|
|||
summary: "Get a list of generated invites",
|
||||
operationId: "AdminAPI.InviteController.index",
|
||||
security: [%{"oAuth" => ["read:invites"]}],
|
||||
parameters: admin_api_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Invites", "application/json", %Schema{
|
||||
|
@ -51,6 +52,7 @@ def create_operation do
|
|||
summary: "Create an account registration invite token",
|
||||
operationId: "AdminAPI.InviteController.create",
|
||||
security: [%{"oAuth" => ["write:invites"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
type: :object,
|
||||
|
@ -71,6 +73,7 @@ def revoke_operation do
|
|||
summary: "Revoke invite by token",
|
||||
operationId: "AdminAPI.InviteController.revoke",
|
||||
security: [%{"oAuth" => ["write:invites"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
|
@ -97,6 +100,7 @@ def email_operation do
|
|||
summary: "Sends registration invite via email",
|
||||
operationId: "AdminAPI.InviteController.email",
|
||||
security: [%{"oAuth" => ["write:invites"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
|
|
|
@ -33,6 +33,7 @@ def index_operation do
|
|||
%Schema{type: :integer, default: 50},
|
||||
"Number of statuses to return"
|
||||
)
|
||||
| admin_api_params()
|
||||
],
|
||||
responses: %{
|
||||
200 => success_response()
|
||||
|
@ -46,6 +47,7 @@ def delete_operation do
|
|||
summary: "Remove a banned MediaProxy URL from Cachex",
|
||||
operationId: "AdminAPI.MediaProxyCacheController.delete",
|
||||
security: [%{"oAuth" => ["write:media_proxy_caches"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
|
@ -71,6 +73,7 @@ def purge_operation do
|
|||
summary: "Purge and optionally ban a MediaProxy URL",
|
||||
operationId: "AdminAPI.MediaProxyCacheController.purge",
|
||||
security: [%{"oAuth" => ["write:media_proxy_caches"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
|
|
|
@ -36,6 +36,7 @@ def index_operation do
|
|||
%Schema{type: :integer, default: 50},
|
||||
"Number of apps to return"
|
||||
)
|
||||
| admin_api_params()
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
|
@ -72,6 +73,7 @@ def create_operation do
|
|||
summary: "Create OAuth App",
|
||||
operationId: "AdminAPI.OAuthAppController.create",
|
||||
requestBody: request_body("Parameters", create_request()),
|
||||
parameters: admin_api_params(),
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("App", "application/json", oauth_app()),
|
||||
|
@ -85,7 +87,7 @@ def update_operation do
|
|||
tags: ["Admin", "oAuth Apps"],
|
||||
summary: "Update OAuth App",
|
||||
operationId: "AdminAPI.OAuthAppController.update",
|
||||
parameters: [id_param()],
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: request_body("Parameters", update_request()),
|
||||
responses: %{
|
||||
|
@ -103,7 +105,7 @@ def delete_operation do
|
|||
tags: ["Admin", "oAuth Apps"],
|
||||
summary: "Delete OAuth App",
|
||||
operationId: "AdminAPI.OAuthAppController.delete",
|
||||
parameters: [id_param()],
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
responses: %{
|
||||
204 => no_content_response(),
|
||||
|
|
|
@ -19,6 +19,7 @@ def index_operation do
|
|||
summary: "List Relays",
|
||||
operationId: "AdminAPI.RelayController.index",
|
||||
security: [%{"oAuth" => ["read"]}],
|
||||
parameters: admin_api_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Response", "application/json", %Schema{
|
||||
|
@ -41,6 +42,7 @@ def follow_operation do
|
|||
summary: "Follow a Relay",
|
||||
operationId: "AdminAPI.RelayController.follow",
|
||||
security: [%{"oAuth" => ["write:follows"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
type: :object,
|
||||
|
@ -64,6 +66,7 @@ def unfollow_operation do
|
|||
summary: "Unfollow a Relay",
|
||||
operationId: "AdminAPI.RelayController.unfollow",
|
||||
security: [%{"oAuth" => ["write:follows"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
type: :object,
|
||||
|
|
|
@ -48,6 +48,7 @@ def index_operation do
|
|||
%Schema{type: :integer, default: 50},
|
||||
"Number number of log entries per page"
|
||||
)
|
||||
| admin_api_params()
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
|
@ -71,7 +72,7 @@ def show_operation do
|
|||
tags: ["Admin", "Reports"],
|
||||
summary: "Get an individual report",
|
||||
operationId: "AdminAPI.ReportController.show",
|
||||
parameters: [id_param()],
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
security: [%{"oAuth" => ["read:reports"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Report", "application/json", report()),
|
||||
|
@ -86,6 +87,7 @@ def update_operation do
|
|||
summary: "Change the state of one or multiple reports",
|
||||
operationId: "AdminAPI.ReportController.update",
|
||||
security: [%{"oAuth" => ["write:reports"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody: request_body("Parameters", update_request(), required: true),
|
||||
responses: %{
|
||||
204 => no_content_response(),
|
||||
|
@ -100,7 +102,7 @@ def notes_create_operation do
|
|||
tags: ["Admin", "Reports"],
|
||||
summary: "Create report note",
|
||||
operationId: "AdminAPI.ReportController.notes_create",
|
||||
parameters: [id_param()],
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
requestBody:
|
||||
request_body("Parameters", %Schema{
|
||||
type: :object,
|
||||
|
@ -124,6 +126,7 @@ def notes_delete_operation do
|
|||
parameters: [
|
||||
Operation.parameter(:report_id, :path, :string, "Report ID"),
|
||||
Operation.parameter(:id, :path, :string, "Note ID")
|
||||
| admin_api_params()
|
||||
],
|
||||
security: [%{"oAuth" => ["write:reports"]}],
|
||||
responses: %{
|
||||
|
|
|
@ -55,6 +55,7 @@ def index_operation do
|
|||
%Schema{type: :integer, default: 50},
|
||||
"Number of statuses to return"
|
||||
)
|
||||
| admin_api_params()
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
|
@ -71,7 +72,7 @@ def show_operation do
|
|||
tags: ["Admin", "Statuses"],
|
||||
summary: "Show Status",
|
||||
operationId: "AdminAPI.StatusController.show",
|
||||
parameters: [id_param()],
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
security: [%{"oAuth" => ["read:statuses"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Status", "application/json", status()),
|
||||
|
@ -85,7 +86,7 @@ def update_operation do
|
|||
tags: ["Admin", "Statuses"],
|
||||
summary: "Change the scope of an individual reported status",
|
||||
operationId: "AdminAPI.StatusController.update",
|
||||
parameters: [id_param()],
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
security: [%{"oAuth" => ["write:statuses"]}],
|
||||
requestBody: request_body("Parameters", update_request(), required: true),
|
||||
responses: %{
|
||||
|
@ -100,7 +101,7 @@ def delete_operation do
|
|||
tags: ["Admin", "Statuses"],
|
||||
summary: "Delete an individual reported status",
|
||||
operationId: "AdminAPI.StatusController.delete",
|
||||
parameters: [id_param()],
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
security: [%{"oAuth" => ["write:statuses"]}],
|
||||
responses: %{
|
||||
200 => empty_object_response(),
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
|
@ -40,48 +39,6 @@ def confirmation_resend_operation do
|
|||
}
|
||||
end
|
||||
|
||||
def update_avatar_operation do
|
||||
%Operation{
|
||||
tags: ["Accounts"],
|
||||
summary: "Set/clear user avatar image",
|
||||
operationId: "PleromaAPI.AccountController.update_avatar",
|
||||
requestBody:
|
||||
request_body("Parameters", update_avatar_or_background_request(), required: true),
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
responses: %{
|
||||
200 => update_response(),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_banner_operation do
|
||||
%Operation{
|
||||
tags: ["Accounts"],
|
||||
summary: "Set/clear user banner image",
|
||||
operationId: "PleromaAPI.AccountController.update_banner",
|
||||
requestBody: request_body("Parameters", update_banner_request(), required: true),
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
responses: %{
|
||||
200 => update_response()
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_background_operation do
|
||||
%Operation{
|
||||
tags: ["Accounts"],
|
||||
summary: "Set/clear user background image",
|
||||
operationId: "PleromaAPI.AccountController.update_background",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
requestBody:
|
||||
request_body("Parameters", update_avatar_or_background_request(), required: true),
|
||||
responses: %{
|
||||
200 => update_response()
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def favourites_operation do
|
||||
%Operation{
|
||||
tags: ["Accounts"],
|
||||
|
@ -136,52 +93,4 @@ defp id_param do
|
|||
required: true
|
||||
)
|
||||
end
|
||||
|
||||
defp update_avatar_or_background_request do
|
||||
%Schema{
|
||||
title: "PleromaAccountUpdateAvatarOrBackgroundRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
img: %Schema{
|
||||
nullable: true,
|
||||
type: :string,
|
||||
format: :binary,
|
||||
description: "Image encoded using `multipart/form-data` or an empty string to clear"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_banner_request do
|
||||
%Schema{
|
||||
title: "PleromaAccountUpdateBannerRequest",
|
||||
type: :object,
|
||||
properties: %{
|
||||
banner: %Schema{
|
||||
type: :string,
|
||||
nullable: true,
|
||||
format: :binary,
|
||||
description: "Image encoded using `multipart/form-data` or an empty string to clear"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_response do
|
||||
Operation.response("PleromaAccountUpdateResponse", "application/json", %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
nullable: true,
|
||||
description: "Image URL"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"url" =>
|
||||
"https://cofe.party/media/9d0add56-bcb6-4c0f-8225-cbbd0b6dd773/13eadb6972c9ccd3f4ffa3b8196f0e0d38b4d2f27594457c52e52946c054cd9a.gif"
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -84,7 +84,7 @@ def delete_operation do
|
|||
operationId: "StatusController.delete",
|
||||
parameters: [id_param()],
|
||||
responses: %{
|
||||
200 => empty_object_response(),
|
||||
200 => status_response(),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
|
|
|
@ -90,11 +90,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
|||
notification_settings: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
followers: %Schema{type: :boolean},
|
||||
follows: %Schema{type: :boolean},
|
||||
non_followers: %Schema{type: :boolean},
|
||||
non_follows: %Schema{type: :boolean},
|
||||
privacy_option: %Schema{type: :boolean}
|
||||
block_from_strangers: %Schema{type: :boolean},
|
||||
hide_notification_contents: %Schema{type: :boolean}
|
||||
}
|
||||
},
|
||||
relationship: AccountRelationship,
|
||||
|
@ -102,6 +99,13 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
|||
type: :object,
|
||||
description:
|
||||
"A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`"
|
||||
},
|
||||
accepts_chat_messages: %Schema{type: :boolean, nullable: true},
|
||||
favicon: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
nullable: true,
|
||||
description: "Favicon image of the user's instance"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -169,16 +173,14 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
|||
"is_admin" => false,
|
||||
"is_moderator" => false,
|
||||
"skip_thread_containment" => false,
|
||||
"accepts_chat_messages" => true,
|
||||
"chat_token" =>
|
||||
"SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc",
|
||||
"unread_conversation_count" => 0,
|
||||
"tags" => [],
|
||||
"notification_settings" => %{
|
||||
"followers" => true,
|
||||
"follows" => true,
|
||||
"non_followers" => true,
|
||||
"non_follows" => true,
|
||||
"privacy_option" => false
|
||||
"block_from_strangers" => false,
|
||||
"hide_notification_contents" => false
|
||||
},
|
||||
"relationship" => %{
|
||||
"blocked_by" => false,
|
||||
|
|
|
@ -62,6 +62,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
}
|
||||
},
|
||||
content: %Schema{type: :string, format: :html, description: "HTML-encoded status content"},
|
||||
text: %Schema{
|
||||
type: :string,
|
||||
description: "Original unformatted content in plain text",
|
||||
nullable: true
|
||||
},
|
||||
created_at: %Schema{
|
||||
type: :string,
|
||||
format: "date-time",
|
||||
|
|
|
@ -186,6 +186,7 @@ defp object(draft) do
|
|||
draft.poll
|
||||
)
|
||||
|> Map.put("emoji", emoji)
|
||||
|> Map.put("source", draft.status)
|
||||
|
||||
%__MODULE__{draft | object: object}
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue