Merge branch 'develop' into refactor/notification_settings

This commit is contained in:
Mark Felder 2020-07-13 13:32:21 -05:00
commit 80c21100db
314 changed files with 5798 additions and 2411 deletions

1
.gitignore vendored
View file

@ -5,6 +5,7 @@
/*.ez /*.ez
/test/uploads /test/uploads
/.elixir_ls /.elixir_ls
/test/fixtures/DSCN0010_tmp.jpg
/test/fixtures/test_tmp.txt /test/fixtures/test_tmp.txt
/test/fixtures/image_tmp.jpg /test/fixtures/image_tmp.jpg
/test/tmp/ /test/tmp/

View file

@ -58,26 +58,25 @@ unit-testing:
alias: postgres alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
script: script:
- apt-get update && apt-get install -y libimage-exiftool-perl
- mix deps.get - mix deps.get
- mix ecto.create - mix ecto.create
- mix ecto.migrate - mix ecto.migrate
- mix coveralls --preload-modules - mix coveralls --preload-modules
# Removed to fix CI issue. In this early state it wasn't adding much value anyway. federated-testing:
# TODO Fix and reinstate federated testing stage: test
# federated-testing: cache: *testing_cache_policy
# stage: test services:
# cache: *testing_cache_policy - name: minibikini/postgres-with-rum:12
# services: alias: postgres
# - name: minibikini/postgres-with-rum:12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
# alias: postgres script:
# command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - mix deps.get
# script: - mix ecto.create
# - mix deps.get - mix ecto.migrate
# - mix ecto.create - epmd -daemon
# - mix ecto.migrate - mix test --trace --only federated
# - epmd -daemon
# - mix test --trace --only federated
unit-testing-rum: unit-testing-rum:
stage: test stage: test
@ -91,6 +90,7 @@ unit-testing-rum:
<<: *global_variables <<: *global_variables
RUM_ENABLED: "true" RUM_ENABLED: "true"
script: script:
- apt-get update && apt-get install -y libimage-exiftool-perl
- mix deps.get - mix deps.get
- mix ecto.create - mix ecto.create
- mix ecto.migrate - mix ecto.migrate

View file

@ -14,7 +14,7 @@
* Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE): * Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE):
* Elixir version (`elixir -v` for from source installations, N/A for OTP): * Elixir version (`elixir -v` for from source installations, N/A for OTP):
* Operating system: * Operating system:
* PostgreSQL version (`postgres -V`): * PostgreSQL version (`psql -V`):
### Bug description ### Bug description

View file

@ -16,7 +16,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
<details> <details>
<summary>API Changes</summary> <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. - **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 - **Breaking:** Notification Settings API for suppressing notifications
has been simplified down to `block_from_strangers`. has been simplified down to `block_from_strangers`.
- **Breaking:** Notification Settings API option for hiding push notification - **Breaking:** Notification Settings API option for hiding push notification
@ -36,6 +43,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added ### 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. - Chats: Added support for federated chats. For details, see the docs.
- ActivityPub: Added support for existing AP ids for instances migrated from Mastodon. - ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
- Instance: Add `background_image` to configuration and `/api/v1/instance` - Instance: Add `background_image` to configuration and `/api/v1/instance`
@ -52,14 +60,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Added `:reject_deletes` group to SimplePolicy - Added `:reject_deletes` group to SimplePolicy
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances - 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 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> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
- Mastodon API: Add pleroma.parents_visible field to statuses.
- Mastodon API: Extended `/api/v1/instance`. - Mastodon API: Extended `/api/v1/instance`.
- Mastodon API: Support for `include_types` in `/api/v1/notifications`. - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint. - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- Mastodon API: Add support for filtering replies in public and home timelines - 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 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: endpoints for create/update/delete OAuth Apps.
- Admin API: endpoint for status view. - Admin API: endpoint for status view.
- OTP: Add command to reload emoji packs - OTP: Add command to reload emoji packs
@ -73,6 +86,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Resolving Peertube accounts with Webfinger - Resolving Peertube accounts with Webfinger
- `blob:` urls not being allowed by connect-src CSP - `blob:` urls not being allowed by connect-src CSP
- Mastodon API: fix `GET /api/v1/notifications` not returning the full result set - 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)] ## [Unreleased (patch)]
@ -214,7 +231,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: `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: 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, 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 - 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: 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. - 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.

View file

@ -33,7 +33,7 @@ ARG DATA=/var/lib/pleroma
RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\
apk update &&\ apk update &&\
apk add imagemagick ncurses postgresql-client &&\ apk add exiftool imagemagick ncurses postgresql-client &&\
adduser --system --shell /bin/false --home ${HOME} pleroma &&\ adduser --system --shell /bin/false --home ${HOME} pleroma &&\
mkdir -p ${DATA}/uploads &&\ mkdir -p ${DATA}/uploads &&\
mkdir -p ${DATA}/static &&\ mkdir -p ${DATA}/static &&\

View file

@ -24,6 +24,7 @@ defmodule Pleroma.LoadTesting.Activities do
@visibility ~w(public private direct unlisted) @visibility ~w(public private direct unlisted)
@types [ @types [
:simple, :simple,
:simple_filtered,
:emoji, :emoji,
:mentions, :mentions,
:hell_thread, :hell_thread,
@ -242,6 +243,15 @@ defp insert_activity(:simple, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status") insert_local_activity(visibility, group, users, "Simple status")
end 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) defp insert_activity(:emoji, visibility, group, users, _opts)
when group in @remote_groups do when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:") insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:")

View file

@ -32,10 +32,22 @@ defp fetch_user(user) do
) )
end 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 defp fetch_timelines(user) do
fetch_home_timeline(user) fetch_home_timeline(user)
fetch_home_timeline_with_filter(user)
fetch_direct_timeline(user) fetch_direct_timeline(user)
fetch_public_timeline(user) fetch_public_timeline(user)
fetch_public_timeline_with_filter(user)
fetch_public_timeline(user, :with_blocks) fetch_public_timeline(user, :with_blocks)
fetch_public_timeline(user, :local) fetch_public_timeline(user, :local)
fetch_public_timeline(user, :tag) fetch_public_timeline(user, :tag)
@ -61,7 +73,7 @@ defp opts_for_home_timeline(user) do
} }
end end
defp fetch_home_timeline(user) do defp fetch_home_timeline(user, title_end \\ "") do
opts = opts_for_home_timeline(user) opts = opts_for_home_timeline(user)
recipients = [user.ap_id | User.following(user)] recipients = [user.ap_id | User.following(user)]
@ -84,9 +96,11 @@ defp fetch_home_timeline(user) do
|> Enum.reverse() |> Enum.reverse()
|> List.last() |> List.last()
title = "home timeline " <> title_end
Benchee.run( Benchee.run(
%{ %{
"home timeline" => fn opts -> ActivityPub.fetch_activities(recipients, opts) end title => fn opts -> ActivityPub.fetch_activities(recipients, opts) end
}, },
inputs: %{ inputs: %{
"1 page" => opts, "1 page" => opts,
@ -108,6 +122,14 @@ defp fetch_home_timeline(user) do
) )
end 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 defp opts_for_direct_timeline(user) do
%{ %{
visibility: "direct", visibility: "direct",
@ -210,6 +232,14 @@ defp fetch_public_timeline(user) do
fetch_public_timeline(opts, "public timeline") fetch_public_timeline(opts, "public timeline")
end 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 defp fetch_public_timeline(user, :local) do
opts = opts_for_public_timeline(user, :local) opts = opts_for_public_timeline(user, :local)

View file

@ -97,6 +97,7 @@
"dat", "dat",
"dweb", "dweb",
"gopher", "gopher",
"hyper",
"ipfs", "ipfs",
"ipns", "ipns",
"irc", "irc",
@ -188,6 +189,7 @@
background_image: "/images/city.jpg", background_image: "/images/city.jpg",
instance_thumbnail: "/instance/thumbnail.jpeg", instance_thumbnail: "/instance/thumbnail.jpeg",
limit: 5_000, limit: 5_000,
description_limit: 5_000,
chat_limit: 5_000, chat_limit: 5_000,
remote_limit: 100_000, remote_limit: 100_000,
upload_limit: 16_000_000, upload_limit: 16_000_000,
@ -436,10 +438,7 @@
config :pleroma, Pleroma.Web.Preload, config :pleroma, Pleroma.Web.Preload,
providers: [ providers: [
Pleroma.Web.Preload.Providers.Instance, Pleroma.Web.Preload.Providers.Instance
Pleroma.Web.Preload.Providers.User,
Pleroma.Web.Preload.Providers.Timelines,
Pleroma.Web.Preload.Providers.StatusNet
] ]
config :pleroma, :http_security, config :pleroma, :http_security,
@ -499,8 +498,7 @@
config :pleroma, Oban, config :pleroma, Oban,
repo: Pleroma.Repo, repo: Pleroma.Repo,
verbose: false, log: false,
prune: {:maxlen, 1500},
queues: [ queues: [
activity_expiration: 10, activity_expiration: 10,
federator_incoming: 50, federator_incoming: 50,
@ -707,6 +705,8 @@
config :ex_aws, http_client: Pleroma.HTTP.ExAws config :ex_aws, http_client: Pleroma.HTTP.ExAws
config :pleroma, :instances_favicons, enabled: false
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View file

@ -23,29 +23,26 @@
key: :uploader, key: :uploader,
type: :module, type: :module,
description: "Module which will be used for uploads", description: "Module which will be used for uploads",
suggestions: [Pleroma.Uploaders.Local, Pleroma.Uploaders.S3] suggestions: {:list_behaviour_implementations, Pleroma.Uploaders.Uploader}
}, },
%{ %{
key: :filters, key: :filters,
type: {:list, :module}, type: {:list, :module},
description: 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.", "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: suggestions: {:list_behaviour_implementations, Pleroma.Upload.Filter}
Generator.list_modules_in_dir(
"lib/pleroma/upload/filter",
"Elixir.Pleroma.Upload.Filter."
)
}, },
%{ %{
key: :link_name, key: :link_name,
type: :boolean, type: :boolean,
description: description:
"If enabled, a name parameter will be added to the url of the upload. For example `https://instance.tld/media/imagehash.png?name=realname.png`." "If enabled, a name parameter will be added to the URL of the upload. For example `https://instance.tld/media/imagehash.png?name=realname.png`."
}, },
%{ %{
key: :base_url, key: :base_url,
label: "Base URL",
type: :string, type: :string,
description: "Base url for the uploads, needed if you use CDN", description: "Base URL for the uploads, needed if you use CDN",
suggestions: [ suggestions: [
"https://cdn-host.com" "https://cdn-host.com"
] ]
@ -58,6 +55,7 @@
}, },
%{ %{
key: :proxy_opts, key: :proxy_opts,
label: "Proxy Options",
type: :keyword, type: :keyword,
description: "Options for Pleroma.ReverseProxy", description: "Options for Pleroma.ReverseProxy",
suggestions: [ suggestions: [
@ -85,6 +83,7 @@
}, },
%{ %{
key: :http, key: :http,
label: "HTTP",
type: :keyword, type: :keyword,
description: "HTTP options", description: "HTTP options",
children: [ children: [
@ -193,7 +192,9 @@
%{ %{
key: :args, key: :args,
type: [:string, {:list, :string}, {:list, :tuple}], 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: [ suggestions: [
"strip", "strip",
"auto-orient", "auto-orient",
@ -479,6 +480,7 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :uri_schemes, key: :uri_schemes,
label: "URI Schemes",
type: :group, type: :group,
description: "URI schemes related settings", description: "URI schemes related settings",
children: [ children: [
@ -492,6 +494,7 @@
"dat", "dat",
"dweb", "dweb",
"gopher", "gopher",
"hyper",
"ipfs", "ipfs",
"ipns", "ipns",
"irc", "irc",
@ -651,17 +654,17 @@
key: :invites_enabled, key: :invites_enabled,
type: :boolean, type: :boolean,
description: description:
"Enable user invitations for admins (depends on `registrations_open` being disabled)." "Enable user invitations for admins (depends on `registrations_open` being disabled)"
}, },
%{ %{
key: :account_activation_required, key: :account_activation_required,
type: :boolean, type: :boolean,
description: "Require users to confirm their emails before signing in." description: "Require users to confirm their emails before signing in"
}, },
%{ %{
key: :federating, key: :federating,
type: :boolean, type: :boolean,
description: "Enable federation with other instances." description: "Enable federation with other instances"
}, },
%{ %{
key: :federation_incoming_replies_max_depth, key: :federation_incoming_replies_max_depth,
@ -679,7 +682,7 @@
label: "Fed. reachability timeout days", label: "Fed. reachability timeout days",
type: :integer, type: :integer,
description: description:
"Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.", "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it",
suggestions: [ suggestions: [
7 7
] ]
@ -693,8 +696,9 @@
key: :public, key: :public,
type: :boolean, type: :boolean,
description: description:
"Makes the client API in authentificated mode-only except for user-profiles." <> "Makes the client API in authenticated mode-only except for user-profiles." <>
" Useful for disabling the Local Timeline and The Whole Known Network." " 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, key: :quarantined_instances,
@ -801,6 +805,7 @@
}, },
%{ %{
key: :safe_dm_mentions, key: :safe_dm_mentions,
label: "Safe DM mentions",
type: :boolean, type: :boolean,
description: description:
"If enabled, only mentions at the beginning of a post will be used to address people in direct messages." <> "If enabled, only mentions at the beginning of a post will be used to address people in direct messages." <>
@ -840,7 +845,7 @@
%{ %{
key: :skip_thread_containment, key: :skip_thread_containment,
type: :boolean, type: :boolean,
description: "Skip filtering out broken threads. Default: enabled" description: "Skip filtering out broken threads. Default: enabled."
}, },
%{ %{
key: :limit_to_local_content, key: :limit_to_local_content,
@ -904,6 +909,7 @@
children: [ children: [
%{ %{
key: :totp, key: :totp,
label: "TOTP settings",
type: :keyword, type: :keyword,
description: "TOTP settings", description: "TOTP settings",
suggestions: [digits: 6, period: 30], suggestions: [digits: 6, period: 30],
@ -920,7 +926,7 @@
type: :integer, type: :integer,
suggestions: [30], suggestions: [30],
description: description:
"a period for which the TOTP code will be valid, in seconds. Defaults to 30 seconds." "A period for which the TOTP code will be valid, in seconds. Defaults to 30 seconds."
} }
] ]
}, },
@ -934,7 +940,7 @@
key: :number, key: :number,
type: :integer, type: :integer,
suggestions: [5], suggestions: [5],
description: "number of backup codes to generate." description: "Number of backup codes to generate."
}, },
%{ %{
key: :length, key: :length,
@ -974,6 +980,7 @@
group: :logger, group: :logger,
type: :group, type: :group,
key: :ex_syslogger, key: :ex_syslogger,
label: "ExSyslogger",
description: "ExSyslogger-related settings", description: "ExSyslogger-related settings",
children: [ children: [
%{ %{
@ -992,7 +999,7 @@
%{ %{
key: :format, key: :format,
type: :string, type: :string,
description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\".", description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\"",
suggestions: ["$metadata[$level] $message"] suggestions: ["$metadata[$level] $message"]
}, },
%{ %{
@ -1006,6 +1013,7 @@
group: :logger, group: :logger,
type: :group, type: :group,
key: :console, key: :console,
label: "Console Logger",
description: "Console logger settings", description: "Console logger settings",
children: [ children: [
%{ %{
@ -1017,7 +1025,7 @@
%{ %{
key: :format, key: :format,
type: :string, type: :string,
description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\".", description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\"",
suggestions: ["$metadata[$level] $message"] suggestions: ["$metadata[$level] $message"]
}, },
%{ %{
@ -1030,6 +1038,7 @@
%{ %{
group: :quack, group: :quack,
type: :group, type: :group,
label: "Quack Logger",
description: "Quack-related settings", description: "Quack-related settings",
children: [ children: [
%{ %{
@ -1058,6 +1067,7 @@
}, },
%{ %{
key: :webhook_url, key: :webhook_url,
label: "Webhook URL",
type: :string, type: :string,
description: "Configure the Slack incoming webhook", description: "Configure the Slack incoming webhook",
suggestions: ["https://hooks.slack.com/services/YOUR-KEY-HERE"] suggestions: ["https://hooks.slack.com/services/YOUR-KEY-HERE"]
@ -1140,19 +1150,19 @@
key: :greentext, key: :greentext,
label: "Greentext", label: "Greentext",
type: :boolean, type: :boolean,
description: "Enables green text on lines prefixed with the > character." description: "Enables green text on lines prefixed with the > character"
}, },
%{ %{
key: :hideFilteredStatuses, key: :hideFilteredStatuses,
label: "Hide Filtered Statuses", label: "Hide Filtered Statuses",
type: :boolean, type: :boolean,
description: "Hides filtered statuses from timelines." description: "Hides filtered statuses from timelines"
}, },
%{ %{
key: :hideMutedPosts, key: :hideMutedPosts,
label: "Hide Muted Posts", label: "Hide Muted Posts",
type: :boolean, type: :boolean,
description: "Hides muted statuses from timelines." description: "Hides muted statuses from timelines"
}, },
%{ %{
key: :hidePostStats, key: :hidePostStats,
@ -1164,7 +1174,7 @@
key: :hideSitename, key: :hideSitename,
label: "Hide Sitename", label: "Hide Sitename",
type: :boolean, type: :boolean,
description: "Hides instance name from PleromaFE banner." description: "Hides instance name from PleromaFE banner"
}, },
%{ %{
key: :hideUserStats, key: :hideUserStats,
@ -1209,14 +1219,14 @@
label: "NSFW Censor Image", label: "NSFW Censor Image",
type: :string, type: :string,
description: description:
"URL of the image to use for hiding NSFW media attachments in the timeline.", "URL of the image to use for hiding NSFW media attachments in the timeline",
suggestions: ["/static/img/nsfw.74818f9.png"] suggestions: ["/static/img/nsfw.74818f9.png"]
}, },
%{ %{
key: :postContentType, key: :postContentType,
label: "Post Content Type", label: "Post Content Type",
type: {:dropdown, :atom}, type: {:dropdown, :atom},
description: "Default post formatting option.", description: "Default post formatting option",
suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"] suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"]
}, },
%{ %{
@ -1245,14 +1255,14 @@
key: :sidebarRight, key: :sidebarRight,
label: "Sidebar on Right", label: "Sidebar on Right",
type: :boolean, type: :boolean,
description: "Change alignment of sidebar and panels to the right." description: "Change alignment of sidebar and panels to the right"
}, },
%{ %{
key: :showFeaturesPanel, key: :showFeaturesPanel,
label: "Show instance features panel", label: "Show instance features panel",
type: :boolean, type: :boolean,
description: description:
"Enables panel displaying functionality of the instance on the About page." "Enables panel displaying functionality of the instance on the About page"
}, },
%{ %{
key: :showInstanceSpecificPanel, key: :showInstanceSpecificPanel,
@ -1310,7 +1320,7 @@
key: :mascots, key: :mascots,
type: {:keyword, :map}, type: {:keyword, :map},
description: description:
"Keyword of mascots, each element must contain both an url and a mime_type key", "Keyword of mascots, each element must contain both an URL and a mime_type key",
suggestions: [ suggestions: [
pleroma_fox_tan: %{ pleroma_fox_tan: %{
url: "/images/pleroma-fox-tan-smol.png", url: "/images/pleroma-fox-tan-smol.png",
@ -1334,7 +1344,7 @@
%{ %{
key: :default_user_avatar, key: :default_user_avatar,
type: :string, type: :string,
description: "URL of the default user avatar.", description: "URL of the default user avatar",
suggestions: ["/images/avi.png"] suggestions: ["/images/avi.png"]
} }
] ]
@ -1344,7 +1354,7 @@
key: :manifest, key: :manifest,
type: :group, type: :group,
description: description:
"This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE", "This section describe PWA manifest instance-specific values. Currently this option relate only for MastoFE.",
children: [ children: [
%{ %{
key: :icons, key: :icons,
@ -1380,10 +1390,45 @@
}, },
%{ %{
group: :pleroma, group: :pleroma,
key: :mrf_simple, key: :mrf,
label: "MRF simple", tab: :mrf,
label: "MRF",
type: :group, type: :group,
description: "Message Rewrite Facility", 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: "Simple ingress policies",
children: [ children: [
%{ %{
key: :media_removal, key: :media_removal,
@ -1402,7 +1447,7 @@
key: :federated_timeline_removal, key: :federated_timeline_removal,
type: {:list, :string}, type: {:list, :string},
description: 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"] suggestions: ["example.com", "*.example.com"]
}, },
%{ %{
@ -1446,14 +1491,15 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :mrf_activity_expiration, key: :mrf_activity_expiration,
tab: :mrf,
label: "MRF Activity Expiration Policy", label: "MRF Activity Expiration Policy",
type: :group, type: :group,
description: "Adds expiration to all local Create Note activities", description: "Adds automatic expiration to all local activities",
children: [ children: [
%{ %{
key: :days, key: :days,
type: :integer, 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] suggestions: [90, 365]
} }
] ]
@ -1461,7 +1507,8 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :mrf_subchain, key: :mrf_subchain,
label: "MRF subchain", tab: :mrf,
label: "MRF Subchain",
type: :group, type: :group,
description: description:
"This policy processes messages through an alternate pipeline when a given message matches certain criteria." <> "This policy processes messages through an alternate pipeline when a given message matches certain criteria." <>
@ -1469,7 +1516,7 @@
children: [ children: [
%{ %{
key: :match_actor, key: :match_actor,
type: :map, type: {:map, {:list, :string}},
description: "Matches a series of regular expressions against the actor field", description: "Matches a series of regular expressions against the actor field",
suggestions: [ suggestions: [
%{ %{
@ -1482,9 +1529,9 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :mrf_rejectnonpublic, key: :mrf_rejectnonpublic,
description: tab: :mrf,
"MRF RejectNonPublic settings. RejectNonPublic drops posts with non-public visibility settings.", description: "RejectNonPublic drops posts with non-public visibility settings.",
label: "MRF reject non public", label: "MRF Reject Non Public",
type: :group, type: :group,
children: [ children: [
%{ %{
@ -1503,16 +1550,17 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :mrf_hellthread, key: :mrf_hellthread,
label: "MRF hellthread", tab: :mrf,
label: "MRF Hellthread",
type: :group, type: :group,
description: "Block messages with too much mentions", description: "Block messages with excessive user mentions",
children: [ children: [
%{ %{
key: :delist_threshold, key: :delist_threshold,
type: :integer, type: :integer,
description: description:
"Number of mentioned users after which the message gets delisted (the message can still be seen, " <> "Number of mentioned users after which the message gets removed from timelines and" <>
" but it will not show up in public timelines and mentioned users won't get notifications about it). Set to 0 to disable.", "disables notifications. Set to 0 to disable.",
suggestions: [10] suggestions: [10]
}, },
%{ %{
@ -1527,27 +1575,28 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :mrf_keyword, key: :mrf_keyword,
label: "MRF keyword", tab: :mrf,
label: "MRF Keyword",
type: :group, type: :group,
description: "Reject or Word-Replace messages with a keyword or regex", description: "Reject or Word-Replace messages with a keyword or regex",
children: [ children: [
%{ %{
key: :reject, key: :reject,
type: [:string, :regex], type: {:list, :string},
description: description:
"A list of patterns which result in message being rejected. Each pattern can be a string or a regular expression.", "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] suggestions: ["foo", ~r/foo/iu]
}, },
%{ %{
key: :federated_timeline_removal, key: :federated_timeline_removal,
type: [:string, :regex], type: {:list, :string},
description: 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.", "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] suggestions: ["foo", ~r/foo/iu]
}, },
%{ %{
key: :replace, key: :replace,
type: [{:tuple, :string, :string}, {:tuple, :regex, :string}], type: {:list, :tuple},
description: description:
"A list of tuples containing {pattern, replacement}. Each pattern can be a string or a regular expression.", "A list of tuples containing {pattern, replacement}. Each pattern can be a string or a regular expression.",
suggestions: [{"foo", "bar"}, {~r/foo/iu, "bar"}] suggestions: [{"foo", "bar"}, {~r/foo/iu, "bar"}]
@ -1557,14 +1606,15 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :mrf_mention, key: :mrf_mention,
label: "MRF mention", tab: :mrf,
label: "MRF Mention",
type: :group, type: :group,
description: "Block messages which mention a user", description: "Block messages which mention a specific user",
children: [ children: [
%{ %{
key: :actors, key: :actors,
type: {:list, :string}, type: {:list, :string},
description: "A list of actors for which any post mentioning them will be dropped.", description: "A list of actors for which any post mentioning them will be dropped",
suggestions: ["actor1", "actor2"] suggestions: ["actor1", "actor2"]
} }
] ]
@ -1572,7 +1622,8 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :mrf_vocabulary, key: :mrf_vocabulary,
label: "MRF vocabulary", tab: :mrf,
label: "MRF Vocabulary",
type: :group, type: :group,
description: "Filter messages which belong to certain activity vocabularies", description: "Filter messages which belong to certain activity vocabularies",
children: [ children: [
@ -1580,14 +1631,14 @@
key: :accept, key: :accept,
type: {:list, :string}, type: {:list, :string},
description: description:
"A list of ActivityStreams terms to accept. If empty, all supported messages are accepted", "A list of ActivityStreams terms to accept. If empty, all supported messages are accepted.",
suggestions: ["Create", "Follow", "Mention", "Announce", "Like"] suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
}, },
%{ %{
key: :reject, key: :reject,
type: {:list, :string}, type: {:list, :string},
description: description:
"A list of ActivityStreams terms to reject. If empty, no messages are rejected", "A list of ActivityStreams terms to reject. If empty, no messages are rejected.",
suggestions: ["Create", "Follow", "Mention", "Announce", "Like"] suggestions: ["Create", "Follow", "Mention", "Announce", "Like"]
} }
] ]
@ -1617,6 +1668,7 @@
}, },
%{ %{
key: :base_url, key: :base_url,
label: "Base URL",
type: :string, type: :string,
description: description:
"The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.", "The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.",
@ -1649,6 +1701,7 @@
}, },
%{ %{
key: :proxy_opts, key: :proxy_opts,
label: "Proxy Options",
type: :keyword, type: :keyword,
description: "Options for Pleroma.ReverseProxy", description: "Options for Pleroma.ReverseProxy",
suggestions: [ suggestions: [
@ -1676,6 +1729,7 @@
}, },
%{ %{
key: :http, key: :http,
label: "HTTP",
type: :keyword, type: :keyword,
description: "HTTP options", description: "HTTP options",
children: [ children: [
@ -1732,15 +1786,20 @@
}, },
%{ %{
key: :headers, key: :headers,
type: {:list, :tuple}, type: {:keyword, :string},
description: "HTTP headers of request.", description: "HTTP headers of request",
suggestions: [{"x-refresh", 1}] suggestions: [{"x-refresh", 1}]
}, },
%{ %{
key: :options, key: :options,
type: :keyword, type: :keyword,
description: "Request options.", description: "Request options",
suggestions: [params: %{ts: "xxx"}] children: [
%{
key: :params,
type: {:map, :string}
}
]
} }
] ]
}, },
@ -1771,6 +1830,7 @@
}, },
%{ %{
key: :ip, key: :ip,
label: "IP",
type: :tuple, type: :tuple,
description: "IP address to bind to", description: "IP address to bind to",
suggestions: [{0, 0, 0, 0}] suggestions: [{0, 0, 0, 0}]
@ -1784,7 +1844,7 @@
%{ %{
key: :dstport, key: :dstport,
type: :integer, type: :integer,
description: "Port advertised in urls (optional, defaults to port)", description: "Port advertised in URLs (optional, defaults to port)",
suggestions: [9999] suggestions: [9999]
} }
] ]
@ -1792,6 +1852,7 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :activitypub, key: :activitypub,
label: "ActivityPub",
type: :group, type: :group,
description: "ActivityPub-related settings", description: "ActivityPub-related settings",
children: [ children: [
@ -1814,7 +1875,7 @@
key: :note_replies_output_limit, key: :note_replies_output_limit,
type: :integer, type: :integer,
description: description:
"The number of Note replies' URIs to be included with outgoing federation (`5` to match Mastodon hardcoded value, `0` to disable the output)." "The number of Note replies' URIs to be included with outgoing federation (`5` to match Mastodon hardcoded value, `0` to disable the output)"
}, },
%{ %{
key: :follow_handshake_timeout, key: :follow_handshake_timeout,
@ -1827,6 +1888,7 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :http_security, key: :http_security,
label: "HTTP security",
type: :group, type: :group,
description: "HTTP security settings", description: "HTTP security settings",
children: [ children: [
@ -1865,7 +1927,7 @@
key: :report_uri, key: :report_uri,
label: "Report URI", label: "Report URI",
type: :string, type: :string,
description: "Adds the specified url to report-uri and report-to group in CSP header", description: "Adds the specified URL to report-uri and report-to group in CSP header",
suggestions: ["https://example.com/report-uri"] suggestions: ["https://example.com/report-uri"]
} }
] ]
@ -1873,9 +1935,10 @@
%{ %{
group: :web_push_encryption, group: :web_push_encryption,
key: :vapid_details, key: :vapid_details,
label: "Vapid Details",
type: :group, type: :group,
description: description:
"Web Push Notifications configuration. You can use the mix task mix web_push.gen.keypair to generate it", "Web Push Notifications configuration. You can use the mix task mix web_push.gen.keypair to generate it.",
children: [ children: [
%{ %{
key: :subject, key: :subject,
@ -1942,6 +2005,7 @@
}, },
%{ %{
group: :pleroma, group: :pleroma,
label: "Pleroma Admin Token",
type: :group, type: :group,
description: 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 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",
@ -1949,7 +2013,7 @@
%{ %{
key: :admin_token, key: :admin_token,
type: :string, type: :string,
description: "Token", description: "Admin token",
suggestions: ["We recommend a secure random string or UUID"] suggestions: ["We recommend a secure random string or UUID"]
} }
] ]
@ -1968,18 +2032,11 @@
""", """,
children: [ children: [
%{ %{
key: :verbose, key: :log,
type: {:dropdown, :atom}, type: {:dropdown, :atom},
description: "Logs verbose mode", description: "Logs verbose mode",
suggestions: [false, :error, :warn, :info, :debug] 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, key: :queues,
type: {:keyword, :integer}, type: {:keyword, :integer},
@ -2114,24 +2171,24 @@
key: :rich_media, key: :rich_media,
type: :group, type: :group,
description: description:
"If enabled the instance will parse metadata from attached links to generate link previews.", "If enabled the instance will parse metadata from attached links to generate link previews",
children: [ children: [
%{ %{
key: :enabled, key: :enabled,
type: :boolean, type: :boolean,
description: "Enables RichMedia parsing of URLs." description: "Enables RichMedia parsing of URLs"
}, },
%{ %{
key: :ignore_hosts, key: :ignore_hosts,
type: {:list, :string}, type: {:list, :string},
description: "List of hosts which will be ignored by the metadata parser.", description: "List of hosts which will be ignored by the metadata parser",
suggestions: ["accounts.google.com", "xss.website"] suggestions: ["accounts.google.com", "xss.website"]
}, },
%{ %{
key: :ignore_tld, key: :ignore_tld,
label: "Ignore TLD", label: "Ignore TLD",
type: {:list, :string}, type: {:list, :string},
description: "List TLDs (top-level domains) which will ignore for parse metadata.", description: "List TLDs (top-level domains) which will ignore for parse metadata",
suggestions: ["local", "localdomain", "lan"] suggestions: ["local", "localdomain", "lan"]
}, },
%{ %{
@ -2159,31 +2216,32 @@
%{ %{
group: :auto_linker, group: :auto_linker,
key: :opts, key: :opts,
label: "Auto Linker",
type: :group, type: :group,
description: "Configuration for the auto_linker library", description: "Configuration for the auto_linker library",
children: [ children: [
%{ %{
key: :class, key: :class,
type: [:string, false], type: [:string, false],
description: "Specify the class to be added to the generated link. Disable to clear", description: "Specify the class to be added to the generated link. Disable to clear.",
suggestions: ["auto-linker", false] suggestions: ["auto-linker", false]
}, },
%{ %{
key: :rel, key: :rel,
type: [:string, false], type: [:string, false],
description: "Override the rel attribute. Disable to clear", description: "Override the rel attribute. Disable to clear.",
suggestions: ["ugc", "noopener noreferrer", false] suggestions: ["ugc", "noopener noreferrer", false]
}, },
%{ %{
key: :new_window, key: :new_window,
type: :boolean, type: :boolean,
description: "Link urls will open in new window/tab" description: "Link URLs will open in new window/tab"
}, },
%{ %{
key: :truncate, key: :truncate,
type: [:integer, false], type: [:integer, false],
description: description:
"Set to a number to truncate urls longer then the number. Truncated urls will end in `..`", "Set to a number to truncate URLs longer then the number. Truncated URLs will end in `..`",
suggestions: [15, false] suggestions: [15, false]
}, },
%{ %{
@ -2194,7 +2252,7 @@
%{ %{
key: :extra, key: :extra,
type: :boolean, type: :boolean,
description: "Link urls with rarely used schemes (magnet, ipfs, irc, etc.)" description: "Link URLs with rarely used schemes (magnet, ipfs, irc, etc.)"
} }
] ]
}, },
@ -2240,6 +2298,7 @@
}, },
%{ %{
group: :pleroma, group: :pleroma,
label: "Pleroma Authenticator",
type: :group, type: :group,
description: "Authenticator", description: "Authenticator",
children: [ children: [
@ -2253,6 +2312,7 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :ldap, key: :ldap,
label: "LDAP",
type: :group, type: :group,
description: description:
"Use LDAP for user authentication. When a user logs in to the Pleroma instance, the name and password" <> "Use LDAP for user authentication. When a user logs in to the Pleroma instance, the name and password" <>
@ -2339,6 +2399,7 @@
}, },
%{ %{
key: :uid, key: :uid,
label: "UID",
type: :string, type: :string,
description: description:
"LDAP attribute name to authenticate the user, e.g. when \"cn\", the filter will be \"cn=username,base\"", "LDAP attribute name to authenticate the user, e.g. when \"cn\", the filter will be \"cn=username,base\"",
@ -2354,6 +2415,7 @@
children: [ children: [
%{ %{
key: :enforce_oauth_admin_scope_usage, key: :enforce_oauth_admin_scope_usage,
label: "Enforce OAuth admin scope usage",
type: :boolean, type: :boolean,
description: description:
"OAuth admin scope requirement toggle. " <> "OAuth admin scope requirement toggle. " <>
@ -2370,6 +2432,7 @@
}, },
%{ %{
key: :oauth_consumer_template, key: :oauth_consumer_template,
label: "OAuth consumer template",
type: :string, type: :string,
description: description:
"OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to" <> "OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to" <>
@ -2378,6 +2441,7 @@
}, },
%{ %{
key: :oauth_consumer_strategies, key: :oauth_consumer_strategies,
label: "OAuth consumer strategies",
type: {:list, :string}, type: {:list, :string},
description: description:
"The list of enabled OAuth consumer strategies. By default it's set by OAUTH_CONSUMER_STRATEGIES environment variable." <> "The list of enabled OAuth consumer strategies. By default it's set by OAUTH_CONSUMER_STRATEGIES environment variable." <>
@ -2451,7 +2515,7 @@
%{ %{
key: :styling, key: :styling,
type: :map, type: :map,
description: "a map with color settings for email templates.", description: "A map with color settings for email templates.",
suggestions: [ suggestions: [
%{ %{
link_color: "#d8a070", link_color: "#d8a070",
@ -2506,14 +2570,14 @@
%{ %{
key: :enabled, key: :enabled,
type: :boolean, type: :boolean,
description: "enables new users admin digest email when `true`", description: "Enables new users admin digest email when `true`"
suggestions: [false]
} }
] ]
}, },
%{ %{
group: :pleroma, group: :pleroma,
key: :oauth2, key: :oauth2,
label: "OAuth2",
type: :group, type: :group,
description: "Configure OAuth 2 provider capabilities", description: "Configure OAuth 2 provider capabilities",
children: [ children: [
@ -2532,7 +2596,7 @@
%{ %{
key: :clean_expired_tokens, key: :clean_expired_tokens,
type: :boolean, type: :boolean,
description: "Enable a background job to clean expired oauth tokens. Default: disabled." description: "Enable a background job to clean expired OAuth tokens. Default: disabled."
} }
] ]
}, },
@ -2556,7 +2620,7 @@
}, },
%{ %{
key: :groups, key: :groups,
type: {:keyword, :string, {:list, :string}}, type: {:keyword, {:list, :string}},
description: description:
"Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the group name" <> "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.", " and the value is the location or array of locations. * can be used as a wildcard.",
@ -2616,6 +2680,7 @@
}, },
%{ %{
key: :relation_id_action, key: :relation_id_action,
label: "Relation ID action",
type: [:tuple, {:list, :tuple}], type: [:tuple, {:list, :tuple}],
description: "For actions on relation with a specific user (follow, unfollow)", description: "For actions on relation with a specific user (follow, unfollow)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]] suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
@ -2629,6 +2694,7 @@
}, },
%{ %{
key: :status_id_action, key: :status_id_action,
label: "Status ID action",
type: [:tuple, {:list, :tuple}], type: [:tuple, {:list, :tuple}],
description: description:
"For fav / unfav or reblog / unreblog actions on the same status by the same user", "For fav / unfav or reblog / unreblog actions on the same status by the same user",
@ -2644,6 +2710,7 @@
}, },
%{ %{
group: :esshd, group: :esshd,
label: "ESSHD",
type: :group, type: :group,
description: description:
"Before enabling this you must add :esshd to mix.exs as one of the extra_applications " <> "Before enabling this you must add :esshd to mix.exs as one of the extra_applications " <>
@ -2682,8 +2749,9 @@
}, },
%{ %{
group: :mime, group: :mime,
label: "Mime Types",
type: :group, type: :group,
description: "Mime types", description: "Mime Types settings",
children: [ children: [
%{ %{
key: :types, key: :types,
@ -2742,6 +2810,7 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :http, key: :http,
label: "HTTP",
type: :group, type: :group,
description: "HTTP settings", description: "HTTP settings",
children: [ children: [
@ -2790,6 +2859,7 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :markup, key: :markup,
label: "Markup Settings",
type: :group, type: :group,
children: [ children: [
%{ %{
@ -2830,8 +2900,9 @@
}, },
%{ %{
group: :pleroma, group: :pleroma,
tab: :mrf,
key: :mrf_normalize_markup, key: :mrf_normalize_markup,
label: "MRF normalize markup", label: "MRF Normalize Markup",
description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.", description: "MRF NormalizeMarkup settings. Scrub configured hypertext markup.",
type: :group, type: :group,
children: [ children: [
@ -2887,6 +2958,7 @@
}, },
%{ %{
group: :cors_plug, group: :cors_plug,
label: "CORS plug config",
type: :group, type: :group,
children: [ children: [
%{ %{
@ -2959,6 +3031,7 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :web_cache_ttl, key: :web_cache_ttl,
label: "Web cache TTL",
type: :group, type: :group,
description: description:
"The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration.", "The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration.",
@ -2981,9 +3054,10 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :static_fe, key: :static_fe,
label: "Static FE",
type: :group, type: :group,
description: description:
"Render profiles and posts using server-generated HTML that is viewable without using JavaScript.", "Render profiles and posts using server-generated HTML that is viewable without using JavaScript",
children: [ children: [
%{ %{
key: :enabled, key: :enabled,
@ -3001,18 +3075,18 @@
%{ %{
key: :post_title, key: :post_title,
type: :map, type: :map,
description: "Configure title rendering.", description: "Configure title rendering",
children: [ children: [
%{ %{
key: :max_length, key: :max_length,
type: :integer, type: :integer,
description: "Maximum number of characters before truncating title.", description: "Maximum number of characters before truncating title",
suggestions: [100] suggestions: [100]
}, },
%{ %{
key: :omission, key: :omission,
type: :string, type: :string,
description: "Replacement which will be used after truncating string.", description: "Replacement which will be used after truncating string",
suggestions: ["..."] suggestions: ["..."]
} }
] ]
@ -3022,8 +3096,11 @@
%{ %{
group: :pleroma, group: :pleroma,
key: :mrf_object_age, key: :mrf_object_age,
label: "MRF Object Age",
tab: :mrf,
type: :group, 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: [ children: [
%{ %{
key: :threshold, key: :threshold,
@ -3036,7 +3113,7 @@
type: {:list, :atom}, type: {:list, :atom},
description: description:
"A list of actions to apply to the post. `:delist` removes the post from public timelines; " <> "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", "`:reject` rejects the message entirely",
suggestions: [:delist, :strip_followers, :reject] suggestions: [:delist, :strip_followers, :reject]
} }
@ -3064,13 +3141,13 @@
%{ %{
key: :workers, key: :workers,
type: :integer, type: :integer,
description: "Number of workers to send notifications.", description: "Number of workers to send notifications",
suggestions: [3] suggestions: [3]
}, },
%{ %{
key: :overflow_workers, key: :overflow_workers,
type: :integer, type: :integer,
description: "Maximum number of workers created if pool is empty.", description: "Maximum number of workers created if pool is empty",
suggestions: [2] suggestions: [2]
} }
] ]
@ -3357,44 +3434,46 @@
key: :strict, key: :strict,
type: :boolean, type: :boolean,
description: description:
"Enables strict input validation (useful in development, not recommended in production)", "Enables strict input validation (useful in development, not recommended in production)"
suggestions: [false]
} }
] ]
}, },
%{ %{
group: :pleroma, group: :pleroma,
key: :mrf, key: :instances_favicons,
type: :group, type: :group,
description: "General MRF settings", description: "Control favicons for instances",
children: [ children: [
%{ %{
key: :policies, key: :enabled,
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",
type: :boolean, type: :boolean,
description: description: "Allow/disallow displaying and getting instances favicons"
"Make the content of your Message Rewrite Facility settings public (via nodeinfo)" }
]
}, },
%{ %{
key: :transparency_exclusions, group: :ex_aws,
label: "MRF transparency exclusions", key: :s3,
type: {:list, :string}, type: :group,
description: descriptions: "S3 service related settings",
"Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.", children: [
suggestions: [ %{
"exclusion.com" key: :access_key_id,
] type: :string,
description: "S3 access key ID",
suggestions: ["AKIAQ8UKHTGIYN7DMWWJ"]
},
%{
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"]
} }
] ]
} }

View file

@ -79,8 +79,8 @@
config :pleroma, Oban, config :pleroma, Oban,
queues: false, queues: false,
prune: :disabled, crontab: false,
crontab: false plugins: false
config :pleroma, Pleroma.ScheduledActivity, config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 2, daily_user_limit: 2,
@ -111,6 +111,8 @@
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
config :pleroma, :instances_favicons, enabled: true
if File.exists?("./config/test.secret.exs") do if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs" import_config "test.secret.exs"
else else

View file

@ -27,6 +27,7 @@ Has these additional fields under the `pleroma` object:
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire - `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
- `thread_muted`: true if the thread the post belongs to is muted - `thread_muted`: true if the thread the post belongs to is muted
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint. - `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
- `parent_visible`: If the parent of this post is visible to the user or not.
## Media Attachments ## Media Attachments
@ -51,21 +52,27 @@ The `id` parameter can also be the `nickname` of the user. This only works in th
Has these additional fields under the `pleroma` object: Has these additional fields under the `pleroma` object:
- `ap_id`: nullable URL string, ActivityPub id of the user
- `background_image`: nullable URL string, background image of the user
- `tags`: Lists an array of tags for the user - `tags`: Lists an array of tags for the user
- `relationship{}`: Includes fields as documented for Mastodon API https://docs.joinmastodon.org/entities/relationship/ - `relationship` (object): Includes fields as documented for Mastodon API https://docs.joinmastodon.org/entities/relationship/
- `is_moderator`: boolean, nullable, true if user is a moderator - `is_moderator`: boolean, nullable, true if user is a moderator
- `is_admin`: boolean, nullable, true if user is an admin - `is_admin`: boolean, nullable, true if user is an admin
- `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated - `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
- `hide_favorites`: boolean, true when the user has hiding favorites enabled
- `hide_followers`: boolean, true when the user has follower hiding enabled - `hide_followers`: boolean, true when the user has follower hiding enabled
- `hide_follows`: boolean, true when the user has follow hiding enabled - `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_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 - `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` - `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 `verify_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 - `deactivated`: boolean, true when the user is deactivated
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts - `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_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. - `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 ### Source
@ -162,7 +169,7 @@ Returns: array of Status.
The maximum number of statuses is limited to 100 per request. 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: Additional parameters can be added to the JSON body/Form data:
@ -177,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. - `pleroma_settings_store` - Opaque user settings to be saved on the backend.
- `skip_thread_containment` - if true, skip filtering out broken threads - `skip_thread_containment` - if true, skip filtering out broken threads
- `allow_following_move` - if true, allows automatically follow moved following accounts - `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. - `discoverable` - if true, discovery of this account in search results and other services is allowed.
- `actor_type` - the type of this account. - `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 ### Pleroma Settings Store
@ -187,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. 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 ## Authentication
@ -215,6 +225,8 @@ Has theses additional parameters (which are the same as in Pleroma-API):
`GET /api/v1/instance` has additional fields `GET /api/v1/instance` has additional fields
- `max_toot_chars`: The maximum characters per post - `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 - `poll_limits`: The limits of polls
- `upload_limit`: The maximum upload file size - `upload_limit`: The maximum upload file size
- `avatar_upload_limit`: The same for avatars - `avatar_upload_limit`: The same for avatars
@ -223,6 +235,7 @@ Has theses additional parameters (which are the same as in Pleroma-API):
- `background_image`: A background image that frontends can use - `background_image`: A background image that frontends can use
- `pleroma.metadata.features`: A list of supported features - `pleroma.metadata.features`: A list of supported features
- `pleroma.metadata.federation`: The federation restrictions of this instance - `pleroma.metadata.federation`: The federation restrictions of this instance
- `pleroma.metadata.fields_limits`: A list of values detailing the length and count limitation for various instance-configurable fields.
- `vapid_public_key`: The public key needed for push messages - `vapid_public_key`: The public key needed for push messages
## Markers ## Markers

View file

@ -57,11 +57,11 @@ mix pleroma.user invites
## Revoke invite ## Revoke invite
```sh tab="OTP" ```sh tab="OTP"
./bin/pleroma_ctl user revoke_invite <token_or_id> ./bin/pleroma_ctl user revoke_invite <token>
``` ```
```sh tab="From Source" ```sh tab="From Source"
mix pleroma.user revoke_invite <token_or_id> mix pleroma.user revoke_invite <token>
``` ```

View file

@ -1,6 +1,6 @@
# Updating your instance # 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: Besides that, doing the following is generally enough:

View file

@ -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. * `notify_email`: Email used for notifications.
* `description`: The instances description, can be seen in nodeinfo and ``/api/v1/instance``. * `description`: The instances description, can be seen in nodeinfo and ``/api/v1/instance``.
* `limit`: Posts character limit (CW/Subject included in the counter). * `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. * `chat_limit`: Character limit of the instance chat messages.
* `remote_limit`: Hard character limit beyond which remote posts will be dropped. * `remote_limit`: Hard character limit beyond which remote posts will be dropped.
* `upload_limit`: File size limit of uploads (except for avatar, background, banner). * `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_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
* `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it. * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
* `allow_relay`: Enable Pleromas Relay, which makes it possible to follow a whole instance. * `allow_relay`: Enable Pleromas 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. * `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``. * `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). * `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 * `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
* `:reject` rejects the message entirely * `:reject` rejects the message entirely
#### mrf_steal_emoji #### :mrf_steal_emoji
* `hosts`: List of hosts to steal emojis from * `hosts`: List of hosts to steal emojis from
* `rejected_shortcodes`: Regex-list of shortcodes to reject * `rejected_shortcodes`: Regex-list of shortcodes to reject
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk * `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
@ -475,7 +476,6 @@ For each pool, the options are:
* `:timeout` - timeout while `gun` will wait for response * `:timeout` - timeout while `gun` will wait for response
* `:max_overflow` - additional workers if pool is under load * `:max_overflow` - additional workers if pool is under load
## Captcha ## Captcha
### Pleroma.Captcha ### Pleroma.Captcha
@ -493,7 +493,7 @@ A built-in captcha provider. Enabled by default.
#### Pleroma.Captcha.Kocaptcha #### Pleroma.Captcha.Kocaptcha
Kocaptcha is a very simple captcha service with a single API endpoint, 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. `https://captcha.kotobank.ch` is hosted by the developer.
* `endpoint`: the Kocaptcha endpoint to use. * `endpoint`: the Kocaptcha endpoint to use.
@ -501,6 +501,7 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end
## Uploads ## Uploads
### Pleroma.Upload ### Pleroma.Upload
* `uploader`: Which one of the [uploaders](#uploaders) to use. * `uploader`: Which one of the [uploaders](#uploaders) to use.
* `filters`: List of [upload filters](#upload-filters) 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` * `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 +514,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`. `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
### Uploaders ### Uploaders
#### Pleroma.Uploaders.Local #### Pleroma.Uploaders.Local
* `uploads`: Which directory to store the user-uploads in, relative to pleromas working directory. * `uploads`: Which directory to store the user-uploads in, relative to pleromas working directory.
#### Pleroma.Uploaders.S3 #### Pleroma.Uploaders.S3
Don't forget to configure [Ex AWS S3](#ex-aws-s3-settings)
* `bucket`: S3 bucket name. * `bucket`: S3 bucket name.
* `bucket_namespace`: S3 bucket namespace. * `bucket_namespace`: S3 bucket namespace.
* `public_endpoint`: S3 endpoint that the user finally accesses(ex. "https://s3.dualstack.ap-northeast-1.amazonaws.com") * `public_endpoint`: S3 endpoint that the user finally accesses(ex. "https://s3.dualstack.ap-northeast-1.amazonaws.com")
@ -525,17 +531,23 @@ For example, when using CDN to S3 virtual host format, set "".
At this time, write CNAME to CDN in public_endpoint. 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. * `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 ### 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 #### Pleroma.Upload.Filter.AnonymizeFilename
This filter replaces the filename (not the path) of an upload. For complete obfuscation, add This filter replaces the filename (not the path) of an upload. For complete obfuscation, add
@ -543,6 +555,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}`. * `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 ## Email
### Pleroma.Emails.Mailer ### Pleroma.Emails.Mailer
@ -970,11 +996,11 @@ config :pleroma, :database_config_whitelist, [
### :restrict_unauthenticated ### :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 * `timelines`: public and federated timelines
* `local`: public timeline * `local`: public timeline
* `federated` * `federated`: federated timeline (includes public timeline)
* `profiles`: user profiles * `profiles`: user profiles
* `local` * `local`
* `remote` * `remote`
@ -982,7 +1008,14 @@ Restrict access for unauthenticated users to timelines (public and federate), us
* `local` * `local`
* `remote` * `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 ## Pleroma.Web.ApiSpec.CastAndValidate
* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`. * `: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

View file

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

View file

@ -3,15 +3,48 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Pleroma do 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" @doc "Common functions to be reused in mix tasks"
def start_pleroma do def start_pleroma do
Pleroma.Config.Holder.save_default()
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
if Pleroma.Config.get(:env) != :test do if Pleroma.Config.get(:env) != :test do
Application.put_env(:logger, :console, level: :debug) Application.put_env(:logger, :console, level: :debug)
end 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 if Pleroma.Config.get(:env) not in [:test, :benchmark] do
pleroma_rebooted?() pleroma_rebooted?()

View file

@ -83,7 +83,7 @@ defp create(group, settings) do
defp migrate_from_db(opts) do defp migrate_from_db(opts) do
if Pleroma.Config.get([:configurable_from_database]) do if Pleroma.Config.get([:configurable_from_database]) do
env = opts[:env] || "prod" env = opts[:env] || Pleroma.Config.get(:env)
config_path = config_path =
if Pleroma.Config.get(:release) do if Pleroma.Config.get(:release) do
@ -105,6 +105,10 @@ defp migrate_from_db(opts) do
:ok = File.close(file) :ok = File.close(file)
System.cmd("mix", ["format", config_path]) System.cmd("mix", ["format", config_path])
shell_info(
"Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs"
)
else else
migration_error() migration_error()
end end
@ -112,7 +116,7 @@ defp migrate_from_db(opts) do
defp migration_error do defp migration_error do
shell_error( 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 end

View file

@ -145,7 +145,7 @@ def run(["gen" | rest]) do
options, options,
:uploads_dir, :uploads_dir,
"What directory should media uploads go in (when using the local uploader)?", "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() |> Path.expand()
@ -154,7 +154,7 @@ def run(["gen" | rest]) do
options, options,
:static_dir, :static_dir,
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?", "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() |> Path.expand()

View file

@ -232,7 +232,7 @@ def run(["tag", nickname | tags]) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags) user = user |> User.tag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(tags)}") shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else else
_ -> _ ->
shell_error("Could not change user tags for #{nickname}") 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 with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags) user = user |> User.untag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(tags)}") shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else else
_ -> _ ->
shell_error("Could not change user tags for #{nickname}") shell_error("Could not change user tags for #{nickname}")

View file

@ -35,13 +35,14 @@ def user_agent do
# See http://elixir-lang.org/docs/stable/elixir/Application.html # See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications # for more information on OTP Applications
def start(_type, _args) do def start(_type, _args) do
Pleroma.Config.Holder.save_default() Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers() Pleroma.HTML.compile_scrubbers()
Config.DeprecationWarnings.warn() Config.DeprecationWarnings.warn()
Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled() Pleroma.Plugs.HTTPSecurityPlug.warn_if_disabled()
Pleroma.ApplicationRequirements.verify!() Pleroma.ApplicationRequirements.verify!()
setup_instrumenters() setup_instrumenters()
load_custom_modules() load_custom_modules()
Pleroma.Docs.JSON.compile()
adapter = Application.get_env(:tesla, :adapter) adapter = Application.get_env(:tesla, :adapter)
@ -162,7 +163,8 @@ defp idempotency_expiration,
defp seconds_valid_interval, defp seconds_valid_interval,
do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid])) 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: %{ do: %{
id: String.to_atom("cachex_" <> type), id: String.to_atom("cachex_" <> type),
start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]}, start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},

View file

@ -12,6 +12,11 @@ defmodule Pleroma.Config.Loader do
:swarm :swarm
] ]
@reject_groups [
:postgrex,
:tesla
]
if Code.ensure_loaded?(Config.Reader) do if Code.ensure_loaded?(Config.Reader) do
@reader Config.Reader @reader Config.Reader
@ -47,7 +52,8 @@ defp filter(configs) do
@spec filter_group(atom(), keyword()) :: keyword() @spec filter_group(atom(), keyword()) :: keyword()
def filter_group(group, configs) do def filter_group(group, configs) do
Enum.reject(configs[group], fn {key, _v} -> 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 end
end end

View file

@ -31,8 +31,8 @@ defmodule Pleroma.Config.TransferTask do
{:pleroma, :gopher, [:enabled]} {:pleroma, :gopher, [:enabled]}
] ]
def start_link(_) do def start_link(restart_pleroma? \\ true) do
load_and_update_env() load_and_update_env([], restart_pleroma?)
if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo) if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
:ignore :ignore
end end

View file

@ -6,16 +6,21 @@ def process(implementation, descriptions) do
implementation.process(descriptions) implementation.process(descriptions)
end end
@spec list_modules_in_dir(String.t(), String.t()) :: [module()] @spec list_behaviour_implementations(behaviour :: module()) :: [module()]
def list_modules_in_dir(dir, start) do def list_behaviour_implementations(behaviour) do
with {:ok, files} <- File.ls(dir) do :code.all_loaded()
files |> Enum.filter(fn {module, _} ->
|> Enum.filter(&String.ends_with?(&1, ".ex")) # This shouldn't be needed as all modules are expected to have module_info/1,
|> Enum.map(fn filename -> # but in test enviroments some transient modules `:elixir_compiler_XX`
module = filename |> String.trim_trailing(".ex") |> Macro.camelize() # are loaded for some reason (where XX is a random integer).
String.to_atom(start <> module) if function_exported?(module, :module_info, 1) do
end) module.module_info(:attributes)
|> Keyword.get_values(:behaviour)
|> List.flatten()
|> Enum.member?(behaviour)
end end
end)
|> Enum.map(fn {module, _} -> module end)
end end
@doc """ @doc """
@ -87,6 +92,12 @@ defp humanize(entity) do
else: string else: string
end end
defp format_suggestions({:list_behaviour_implementations, behaviour}) do
behaviour
|> list_behaviour_implementations()
|> format_suggestions()
end
defp format_suggestions([]), do: [] defp format_suggestions([]), do: []
defp format_suggestions([suggestion | tail]) do defp format_suggestions([suggestion | tail]) do

View file

@ -1,5 +1,19 @@
defmodule Pleroma.Docs.JSON do defmodule Pleroma.Docs.JSON do
@behaviour Pleroma.Docs.Generator @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()} @spec process(keyword()) :: {:ok, String.t()}
def process(descriptions) do def process(descriptions) do
@ -13,11 +27,4 @@ def process(descriptions) do
{:ok, path} {:ok, path}
end end
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 end

View file

@ -68,6 +68,11 @@ defp print_suggestion(file, suggestion, as_list \\ false) do
IO.write(file, " #{list_mark}`#{inspect(suggestion)}`\n") IO.write(file, " #{list_mark}`#{inspect(suggestion)}`\n")
end 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, nil), do: nil
defp print_suggestions(_file, ""), do: nil defp print_suggestions(_file, ""), do: nil

View file

@ -10,7 +10,7 @@ defmodule Pleroma.Emails.AdminEmail do
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Web.Router.Helpers 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_name, do: instance_config()[:name]
defp instance_notify_email do 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> <p>Reported Account: <a href="#{user_url(account)}">#{account.nickname}</a></p>
#{comment_html} #{comment_html}
#{statuses_html} #{statuses_html}
<p>
<a href="#{Pleroma.Web.base_url()}/pleroma/admin/#/reports/index">View Reports in AdminFE</a>
""" """
new() new()

View file

@ -108,7 +108,7 @@ defp load_pack(pack_dir, emoji_groups) do
if File.exists?(emoji_txt) do if File.exists?(emoji_txt) do
load_from_file(emoji_txt, emoji_groups) load_from_file(emoji_txt, emoji_groups)
else else
extensions = Pleroma.Config.get([:emoji, :pack_extensions]) extensions = Config.get([:emoji, :pack_extensions])
Logger.info( Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{ "No emoji.txt found for pack \"#{pack_name}\", assuming all #{

View file

@ -34,10 +34,18 @@ def get(id, %{id: user_id} = _user) do
Repo.one(query) Repo.one(query)
end 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 = query =
from( from(
f in Pleroma.Filter, f in query,
where: f.user_id == ^user_id, where: f.user_id == ^user_id,
order_by: [desc: :id] order_by: [desc: :id]
) )
@ -95,4 +103,34 @@ def update(%Pleroma.Filter{} = filter, params) do
|> validate_required([:phrase, :context]) |> validate_required([:phrase, :context])
|> Repo.update() |> Repo.update()
end 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 end

View file

@ -109,7 +109,7 @@ def extract_first_external_url(object, content) do
result = result =
content content
|> Floki.parse_fragment!() |> Floki.parse_fragment!()
|> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]") |> Floki.filter_out("a.mention,a.hashtag,a.attachment,a[rel~=\"tag\"]")
|> Floki.attribute("a", "href") |> Floki.attribute("a", "href")
|> Enum.at(0) |> Enum.at(0)

View file

@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do
schema "instances" do schema "instances" do
field(:host, :string) field(:host, :string)
field(:unreachable_since, :naive_datetime_usec) field(:unreachable_since, :naive_datetime_usec)
field(:favicon, :string)
field(:favicon_updated_at, :naive_datetime)
timestamps() timestamps()
end end
@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do
def changeset(struct, params \\ %{}) do def changeset(struct, params \\ %{}) do
struct struct
|> cast(params, [:host, :unreachable_since]) |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])
|> validate_required([:host]) |> validate_required([:host])
|> unique_constraint(:host) |> unique_constraint(:host)
end end
@ -120,4 +122,48 @@ defp parse_datetime(datetime) when is_binary(datetime) do
end end
defp parse_datetime(datetime), do: datetime 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 end

View file

@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MigrationHelper.NotificationBackfill do defmodule Pleroma.MigrationHelper.NotificationBackfill do
alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
@ -25,18 +24,27 @@ def fill_in_notification_types do
|> type_from_activity() |> type_from_activity()
notification notification
|> Notification.changeset(%{type: type}) |> Ecto.Changeset.change(%{type: type})
|> Repo.update() |> Repo.update()
end) end)
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. # This is copied over from Notifications to keep this stable.
defp type_from_activity(%{data: %{"type" => type}} = activity) do defp type_from_activity(%{data: %{"type" => type}} = activity) do
case type do case type do
"Follow" -> "Follow" ->
accepted_function = fn activity -> accepted_function = fn activity ->
with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]), with %User{} = follower <- get_by_ap_id(activity.data["actor"]),
%User{} = followed <- User.get_by_ap_id(activity.data["object"]) do %User{} = followed <- get_by_ap_id(activity.data["object"]) do
Pleroma.FollowingRelationship.following?(follower, followed) Pleroma.FollowingRelationship.following?(follower, followed)
end end
end end

View file

@ -130,6 +130,7 @@ def for_user_query(user, opts \\ %{}) do
|> preload([n, a, o], activity: {a, object: o}) |> preload([n, a, o], activity: {a, object: o})
|> exclude_notification_muted(user, exclude_notification_muted_opts) |> exclude_notification_muted(user, exclude_notification_muted_opts)
|> exclude_blocked(user, exclude_blocked_opts) |> exclude_blocked(user, exclude_blocked_opts)
|> exclude_filtered(user)
|> exclude_visibility(opts) |> exclude_visibility(opts)
end end
@ -158,6 +159,20 @@ defp exclude_notification_muted(query, user, opts) do
|> where([n, a, o, tm], is_nil(tm.user_id)) |> where([n, a, o, tm], is_nil(tm.user_id))
end 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] @valid_visibilities ~w[direct unlisted public private]
defp exclude_visibility(query, %{exclude_visibilities: visibility}) defp exclude_visibility(query, %{exclude_visibilities: visibility})
@ -337,6 +352,7 @@ def dismiss(%{id: user_id} = _user, id) do
end end
end end
@spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
def create_notifications(activity, options \\ []) def create_notifications(activity, options \\ [])
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
@ -367,6 +383,7 @@ defp do_create_notifications(%Activity{} = activity, options) do
do_send = do_send && user in enabled_receivers do_send = do_send && user in enabled_receivers
create_notification(activity, user, do_send) create_notification(activity, user, do_send)
end) end)
|> Enum.reject(&is_nil/1)
{:ok, notifications} {:ok, notifications}
end end
@ -480,6 +497,10 @@ def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_i
end end
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 def get_potential_receiver_ap_ids(activity) do
[] []
|> Utils.maybe_notify_to_recipients(activity) |> Utils.maybe_notify_to_recipients(activity)
@ -551,7 +572,8 @@ def skip?(%Activity{} = activity, %User{} = user) do
:self, :self,
:invisible, :invisible,
:block_from_strangers, :block_from_strangers,
:recently_followed :recently_followed,
:filtered
] ]
|> Enum.find(&skip?(&1, activity, user)) |> Enum.find(&skip?(&1, activity, user))
end end
@ -590,6 +612,26 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity,
end) end)
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 skip?(_, _, _), do: false
def for_user_and_activity(user, activity) do def for_user_and_activity(user, activity) do

View file

@ -83,8 +83,8 @@ def fetch_object_from_id(id, options \\ []) do
{:transmogrifier, {:error, {:reject, nil}}} -> {:transmogrifier, {:error, {:reject, nil}}} ->
{:reject, nil} {:reject, nil}
{:transmogrifier, _} -> {:transmogrifier, _} = e ->
{:error, "Transmogrifier failure."} {:error, e}
{:object, data, nil} -> {:object, data, nil} ->
reinject_object(%Object{}, data) reinject_object(%Object{}, data)

View file

@ -69,10 +69,11 @@ defp csp_string do
img_src = "img-src 'self' data: blob:" img_src = "img-src 'self' data: blob:"
media_src = "media-src 'self'" media_src = "media-src 'self'"
# Strict multimedia CSP enforcement only when MediaProxy is enabled
{img_src, media_src} = {img_src, media_src} =
if Config.get([:media_proxy, :enabled]) && if Config.get([:media_proxy, :enabled]) &&
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do !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]} {[img_src, sources], [media_src, sources]}
else else
{[img_src, " https:"], [media_src, " https:"]} {[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 = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
connect_src = connect_src =
if Pleroma.Config.get(:env) == :dev do if Config.get(:env) == :dev do
[connect_src, " http://localhost:3035/"] [connect_src, " http://localhost:3035/"]
else else
connect_src connect_src
end end
script_src = script_src =
if Pleroma.Config.get(:env) == :dev do if Config.get(:env) == :dev do
"script-src 'self' 'unsafe-eval'" "script-src 'self' 'unsafe-eval'"
else else
"script-src 'self'" "script-src 'self'"
@ -107,29 +108,28 @@ defp csp_string do
|> :erlang.iolist_to_binary() |> :erlang.iolist_to_binary()
end end
defp get_proxy_and_attachment_sources do defp build_csp_multimedia_source_list do
media_proxy_whitelist = media_proxy_whitelist =
Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc -> Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
add_source(acc, host) add_source(acc, host)
end) end)
media_proxy_base_url = media_proxy_base_url = build_csp_param(Config.get([:media_proxy, :base_url]))
if Config.get([:media_proxy, :base_url]),
do: URI.parse(Config.get([:media_proxy, :base_url])).host
upload_base_url = upload_base_url = build_csp_param(Config.get([Pleroma.Upload, :base_url]))
if Config.get([Pleroma.Upload, :base_url]),
do: URI.parse(Config.get([Pleroma.Upload, :base_url])).host
s3_endpoint = s3_endpoint = build_csp_param(Config.get([Pleroma.Uploaders.S3, :public_endpoint]))
if Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.S3,
do: URI.parse(Config.get([Pleroma.Uploaders.S3, :public_endpoint])).host captcha_method = Config.get([Pleroma.Captcha, :method])
captcha_endpoint = build_csp_param(Config.get([captcha_method, :endpoint]))
[] []
|> add_source(media_proxy_base_url) |> add_source(media_proxy_base_url)
|> add_source(upload_base_url) |> add_source(upload_base_url)
|> add_source(s3_endpoint) |> add_source(s3_endpoint)
|> add_source(media_proxy_whitelist) |> add_source(media_proxy_whitelist)
|> add_source(captcha_endpoint)
end end
defp add_source(iodata, nil), do: iodata defp add_source(iodata, nil), do: iodata
@ -139,6 +139,16 @@ defp add_csp_param(csp_iodata, nil), do: csp_iodata
defp add_csp_param(csp_iodata, param), do: [[param, ?;] | 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 def warn_if_disabled do
unless Config.get([:http_security, :enabled]) do unless Config.get([:http_security, :enabled]) do
Logger.warn(" Logger.warn("

View file

@ -9,7 +9,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do
def init(options), do: options def init(options), do: options
def call(conn, _) do def call(conn, _) do
if enabled?() and accepts_html?(conn) do if enabled?() and requires_html?(conn) do
conn conn
|> StaticFEController.call(:show) |> StaticFEController.call(:show)
|> halt() |> halt()
@ -20,10 +20,7 @@ def call(conn, _) do
defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false) defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
defp accepts_html?(conn) do defp requires_html?(conn) do
case get_req_header(conn, "accept") do Phoenix.Controller.get_format(conn) == "html"
[accept | _] -> String.contains?(accept, "text/html")
_ -> false
end
end end
end end

View file

@ -3,12 +3,13 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy do defmodule Pleroma.ReverseProxy do
@range_headers ~w(range if-range)
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++ @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) @resp_cache_headers ~w(etag date last-modified)
@keep_resp_headers @resp_cache_headers ++ @keep_resp_headers @resp_cache_headers ++
~w(content-type content-disposition content-encoding content-range) ++ ~w(content-length content-type content-disposition content-encoding) ++
~w(accept-ranges vary) ~w(content-range accept-ranges vary)
@default_cache_control_header "public, max-age=1209600" @default_cache_control_header "public, max-age=1209600"
@valid_resp_codes [200, 206, 304] @valid_resp_codes [200, 206, 304]
@max_read_duration :timer.seconds(30) @max_read_duration :timer.seconds(30)
@ -170,6 +171,8 @@ defp request(method, url, headers, opts) do
end end
defp response(conn, client, url, status, headers, opts) do defp response(conn, client, url, status, headers, opts) do
Logger.debug("#{__MODULE__} #{status} #{url} #{inspect(headers)}")
result = result =
conn conn
|> put_resp_headers(build_resp_headers(headers, opts)) |> put_resp_headers(build_resp_headers(headers, opts))
@ -220,7 +223,9 @@ defp chunk_reply(conn, client, opts, sent_so_far, duration) do
end end
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 conn
|> put_resp_headers(build_resp_headers(headers, opts)) |> put_resp_headers(build_resp_headers(headers, opts))
|> send_resp(code, "") |> send_resp(code, "")
@ -262,9 +267,23 @@ defp build_req_headers(headers, opts) do
headers headers
|> downcase_headers() |> downcase_headers()
|> Enum.filter(fn {k, _} -> k in @keep_req_headers end) |> Enum.filter(fn {k, _} -> k in @keep_req_headers end)
|> (fn headers -> |> build_req_range_or_encoding_header(opts)
headers = headers ++ Keyword.get(opts, :req_headers, []) |> build_req_user_agent_header(opts)
|> Keyword.merge(Keyword.get(opts, :req_headers, []))
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 if Keyword.get(opts, :keep_user_agent, false) do
List.keystore( List.keystore(
headers, headers,
@ -275,7 +294,6 @@ defp build_req_headers(headers, opts) do
else else
headers headers
end end
end).()
end end
defp build_resp_headers(headers, opts) do defp build_resp_headers(headers, opts) do
@ -283,7 +301,7 @@ defp build_resp_headers(headers, opts) do
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end) |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|> build_resp_cache_headers(opts) |> build_resp_cache_headers(opts)
|> build_resp_content_disposition_header(opts) |> build_resp_content_disposition_header(opts)
|> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).() |> Keyword.merge(Keyword.get(opts, :resp_headers, []))
end end
defp build_resp_cache_headers(headers, _opts) do defp build_resp_cache_headers(headers, _opts) do

View file

@ -63,6 +63,10 @@ def store(upload, opts \\ []) do
with {:ok, upload} <- prepare_upload(upload, opts), with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload), {: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, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok, {:ok,
%{ %{
@ -75,9 +79,12 @@ def store(upload, opts \\ []) do
"href" => url_from_spec(upload, opts.base_url, url_spec) "href" => url_from_spec(upload, opts.base_url, url_spec)
} }
], ],
"name" => Map.get(opts, :description) || upload.name "name" => description
}} }}
else else
{:description_limit, _} ->
{:error, :description_too_long}
{:error, error} -> {:error, error} ->
Logger.error( Logger.error(
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}" "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"

View file

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

View file

@ -89,7 +89,7 @@ defmodule Pleroma.User do
field(:keys, :string) field(:keys, :string)
field(:public_key, :string) field(:public_key, :string)
field(:ap_id, :string) field(:ap_id, :string)
field(:avatar, :map) field(:avatar, :map, default: %{})
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
field(:follower_address, :string) field(:follower_address, :string)
field(:following_address, :string) field(:following_address, :string)
@ -115,7 +115,7 @@ defmodule Pleroma.User do
field(:is_moderator, :boolean, default: false) field(:is_moderator, :boolean, default: false)
field(:is_admin, :boolean, default: false) field(:is_admin, :boolean, default: false)
field(:show_role, :boolean, default: true) field(:show_role, :boolean, default: true)
field(:settings, :map, default: nil) field(:mastofe_settings, :map, default: nil)
field(:uri, ObjectValidators.Uri, default: nil) field(:uri, ObjectValidators.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false) field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false) field(:hide_follows_count, :boolean, default: false)
@ -138,6 +138,7 @@ defmodule Pleroma.User do
field(:also_known_as, {:array, :string}, default: []) field(:also_known_as, {:array, :string}, default: [])
field(:inbox, :string) field(:inbox, :string)
field(:shared_inbox, :string) field(:shared_inbox, :string)
field(:accepts_chat_messages, :boolean, default: nil)
embeds_one( embeds_one(
:notification_settings, :notification_settings,
@ -388,8 +389,8 @@ defp fix_follower_address(%{nickname: nickname} = params),
defp fix_follower_address(params), do: params defp fix_follower_address(params), do: params
def remote_user_changeset(struct \\ %User{local: false}, params) do def remote_user_changeset(struct \\ %User{local: false}, params) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Config.get([:instance, :user_name_length], 100)
name = name =
case params[:name] do case params[:name] do
@ -436,7 +437,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
:discoverable, :discoverable,
:invisible, :invisible,
:actor_type, :actor_type,
:also_known_as :also_known_as,
:accepts_chat_messages
] ]
) )
|> validate_required([:name, :ap_id]) |> validate_required([:name, :ap_id])
@ -448,8 +450,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
end end
def update_changeset(struct, params \\ %{}) do def update_changeset(struct, params \\ %{}) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Config.get([:instance, :user_name_length], 100)
struct struct
|> cast( |> cast(
@ -481,7 +483,8 @@ def update_changeset(struct, params \\ %{}) do
:pleroma_settings_store, :pleroma_settings_store,
:discoverable, :discoverable,
:actor_type, :actor_type,
:also_known_as :also_known_as,
:accepts_chat_messages
] ]
) )
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
@ -527,11 +530,21 @@ defp parse_fields(value) do
end end
defp put_emoji(changeset) do defp put_emoji(changeset) do
bio = get_change(changeset, :bio) emojified_fields = [:bio, :name, :raw_fields]
name = get_change(changeset, :name)
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) put_change(changeset, :emoji, emoji)
else else
changeset changeset
@ -539,15 +552,12 @@ defp put_emoji(changeset) do
end end
defp put_change_if_present(changeset, map_field, value_function) do defp put_change_if_present(changeset, map_field, value_function) do
if value = get_change(changeset, map_field) do with {:ok, value} <- fetch_change(changeset, map_field),
with {:ok, new_value} <- value_function.(value) do {:ok, new_value} <- value_function.(value) do
put_change(changeset, map_field, new_value) put_change(changeset, map_field, new_value)
else else
_ -> changeset _ -> changeset
end end
else
changeset
end
end end
defp put_upload(value, type) do defp put_upload(value, type) do
@ -621,12 +631,13 @@ def force_password_reset_async(user) do
def force_password_reset(user), do: update_password_reset_pending(user, true) def force_password_reset(user), do: update_password_reset_pending(user, true)
def register_changeset(struct, params \\ %{}, opts \\ []) do def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Config.get([:instance, :user_name_length], 100)
params = Map.put_new(params, :accepts_chat_messages, true)
need_confirmation? = need_confirmation? =
if is_nil(opts[:need_confirmation]) do if is_nil(opts[:need_confirmation]) do
Pleroma.Config.get([:instance, :account_activation_required]) Config.get([:instance, :account_activation_required])
else else
opts[:need_confirmation] opts[:need_confirmation]
end end
@ -641,13 +652,14 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
:nickname, :nickname,
:password, :password,
:password_confirmation, :password_confirmation,
:emoji :emoji,
:accepts_chat_messages
]) ])
|> validate_required([:name, :nickname, :password, :password_confirmation]) |> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password) |> validate_confirmation(:password)
|> unique_constraint(:email) |> unique_constraint(:email)
|> unique_constraint(:nickname) |> 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(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex) |> validate_format(:email, @email_regex)
|> validate_length(:bio, max: bio_limit) |> 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, true), do: changeset
def maybe_validate_required_email(changeset, _) do 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]) validate_required(changeset, [:email])
else else
changeset changeset
@ -682,7 +694,7 @@ defp put_following_and_follower_address(changeset) do
end end
defp autofollow_users(user) do defp autofollow_users(user) do
candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) candidates = Config.get([:instance, :autofollowed_nicknames])
autofollowed_users = autofollowed_users =
User.Query.build(%{nickname: candidates, local: true, deactivated: false}) 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 def try_send_confirmation_email(%User{} = user) do
if user.confirmation_pending && if user.confirmation_pending &&
Pleroma.Config.get([:instance, :account_activation_required]) do Config.get([:instance, :account_activation_required]) do
user user
|> Pleroma.Emails.UserEmail.account_confirmation_email() |> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async() |> Pleroma.Emails.Mailer.deliver_async()
@ -766,7 +778,7 @@ def follow_all(follower, followeds) do
defdelegate following(user), to: FollowingRelationship defdelegate following(user), to: FollowingRelationship
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do 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 cond do
followed.deactivated -> followed.deactivated ->
@ -967,7 +979,7 @@ def get_cached_by_nickname(nickname) do
end end
def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do 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 cond do
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) -> 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()} @spec update_follower_count(User.t()) :: {:ok, User.t()}
def update_follower_count(%User{} = user) do 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) follower_count = FollowingRelationship.follower_count(user)
user user
@ -1176,7 +1188,7 @@ def update_follower_count(%User{} = user) do
@spec update_following_count(User.t()) :: {:ok, User.t()} @spec update_following_count(User.t()) :: {:ok, User.t()}
def update_following_count(%User{local: false} = user) do 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)} {:ok, maybe_fetch_follow_information(user)}
else else
{:ok, user} {:ok, user}
@ -1263,7 +1275,7 @@ def unmute(%User{} = muter, %User{} = mutee) do
end end
def subscribe(%User{} = subscriber, %User{} = target) do 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 if blocks?(target, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{target.nickname} is blocking you"} {:error, "Could not subscribe: #{target.nickname} is blocking you"}
@ -1309,7 +1321,8 @@ def block(%User{} = blocker, %User{} = blocked) do
unsubscribe(blocked, blocker) unsubscribe(blocked, blocker)
if following?(blocked, blocker), do: unfollow(blocked, blocker) unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
{:ok, blocker} = update_follower_count(blocker) {:ok, blocker} = update_follower_count(blocker)
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked) {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
@ -1527,8 +1540,7 @@ def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
blocked_identifiers, blocked_identifiers,
fn blocked_identifier -> fn blocked_identifier ->
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, _user_block} <- block(blocker, blocked), {:ok, _block} <- CommonAPI.block(blocker, blocked) do
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked blocked
else else
err -> err ->
@ -1546,7 +1558,7 @@ def perform(:follow_import, %User{} = follower, followed_identifiers)
fn followed_identifier -> fn followed_identifier ->
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier), with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed), {:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
followed followed
else else
err -> err ->
@ -1654,7 +1666,7 @@ def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText Pleroma.HTML.Scrubber.TwitterText
end 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) 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 end
defp local_nickname_regex do 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 @extended_local_nickname_regex
else else
@strict_local_nickname_regex @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 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
# use instance-default # use instance-default
config = Pleroma.Config.get([:assets, :mascots]) config = Config.get([:assets, :mascots])
default_mascot = Pleroma.Config.get([:assets, :default_mascot]) default_mascot = Config.get([:assets, :default_mascot])
mascot = Keyword.get(config, 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 def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields 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 changeset
|> validate_length(:fields, max: limit) |> validate_length(:fields, max: limit)
@ -2074,8 +2086,8 @@ def validate_fields(changeset, remote? \\ false) do
end end
defp valid_field?(%{"name" => name, "value" => value}) do defp valid_field?(%{"name" => name, "value" => value}) do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255) name_limit = Config.get([:instance, :account_field_name_length], 255)
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) value_limit = Config.get([:instance, :account_field_value_length], 255)
is_binary(name) && is_binary(value) && String.length(name) <= name_limit && is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
String.length(value) <= value_limit String.length(value) <= value_limit
@ -2085,10 +2097,10 @@ defp valid_field?(_), do: false
defp truncate_field(%{"name" => name, "value" => value}) do defp truncate_field(%{"name" => name, "value" => value}) do
{name, _chopped} = {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} = {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} %{"name" => name, "value" => value}
end end
@ -2118,8 +2130,8 @@ def mascot_update(user, url) do
def mastodon_settings_update(user, settings) do def mastodon_settings_update(user, settings) do
user user
|> cast(%{settings: settings}, [:settings]) |> cast(%{mastofe_settings: settings}, [:mastofe_settings])
|> validate_required([:settings]) |> validate_required([:mastofe_settings])
|> update_and_set_cache() |> update_and_set_cache()
end end
@ -2143,7 +2155,7 @@ def confirmation_changeset(user, need_confirmation: need_confirmation?) do
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
if id not in user.pinned_activities do if id not in user.pinned_activities do
max_pinned_statuses = 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]} params = %{pinned_activities: user.pinned_activities ++ [id]}
user user

View file

@ -52,6 +52,7 @@ defp search_query(query_string, for_user, following) do
|> base_query(following) |> base_query(following)
|> filter_blocked_user(for_user) |> filter_blocked_user(for_user)
|> filter_invisible_users() |> filter_invisible_users()
|> filter_internal_users()
|> filter_blocked_domains(for_user) |> filter_blocked_domains(for_user)
|> fts_search(query_string) |> fts_search(query_string)
|> trigram_rank(query_string) |> trigram_rank(query_string)
@ -68,11 +69,15 @@ defp fts_search(query, query_string) do
u in query, u in query,
where: where:
fragment( 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.nickname,
u.name,
^query_string ^query_string
) )
) )
@ -87,15 +92,23 @@ defp to_tsquery(query_string) do
|> Enum.join(" | ") |> Enum.join(" | ")
end end
# Considers nickname match, localized nickname match, name match; preferences nickname match
defp trigram_rank(query, query_string) do defp trigram_rank(query, query_string) do
from( from(
u in query, u in query,
select_merge: %{ select_merge: %{
search_rank: search_rank:
fragment( fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))", """
similarity(?, ?) +
similarity(?, regexp_replace(?, '@.+', '')) +
similarity(?, trim(coalesce(?, '')))
""",
^query_string, ^query_string,
u.nickname, u.nickname,
^query_string,
u.nickname,
^query_string,
u.name u.name
) )
} }
@ -109,6 +122,10 @@ defp filter_invisible_users(query) do
from(q in query, where: q.invisible == false) from(q in query, where: q.invisible == false)
end 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 defp filter_blocked_user(query, %User{} = blocker) do
query query
|> join(:left, [u], b in Pleroma.UserRelationship, |> join(:left, [u], b in Pleroma.UserRelationship,

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Constants alias Pleroma.Constants
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Filter
alias Pleroma.Maps alias Pleroma.Maps
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
@ -321,28 +322,6 @@ defp accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
end end
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()) :: @spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | nil | {:error, any()} {:ok, Activity.t()} | nil | {:error, any()}
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
@ -366,33 +345,6 @@ defp do_unfollow(follower, followed, activity_id, local) do
end end
end end
@spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_block(blocker, blocked, activity_id, local) end) do
result
end
end
defp do_block(blocker, blocked, activity_id, local) do
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
if unfollow_blocked and fetch_latest_follow(blocker, blocked) do
unfollow(blocker, blocked, nil, local)
end
block_data = make_block_data(blocker, blocked, activity_id)
with {:ok, activity} <- insert(block_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:error, error} -> Repo.rollback(error)
end
end
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()} @spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
def flag( def flag(
%{ %{
@ -473,6 +425,7 @@ def fetch_activities_for_context_query(context, opts) do
|> maybe_set_thread_muted_field(opts) |> maybe_set_thread_muted_field(opts)
|> restrict_blocked(opts) |> restrict_blocked(opts)
|> restrict_recipients(recipients, opts[:user]) |> restrict_recipients(recipients, opts[:user])
|> restrict_filtered(opts)
|> where( |> where(
[activity], [activity],
fragment( fragment(
@ -988,6 +941,26 @@ defp restrict_instance(query, %{instance: instance}) do
defp restrict_instance(query, _), do: query 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, %{include_poll_votes: true}), do: query
defp exclude_poll_votes(query, _) do defp exclude_poll_votes(query, _) do
@ -1118,6 +1091,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_favorited_by(opts) |> restrict_favorited_by(opts)
|> restrict_blocked(restrict_blocked_opts) |> restrict_blocked(restrict_blocked_opts)
|> restrict_muted(restrict_muted_opts) |> restrict_muted(restrict_muted_opts)
|> restrict_filtered(opts)
|> restrict_media(opts) |> restrict_media(opts)
|> restrict_visibility(opts) |> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config) |> restrict_thread_visibility(opts, config)
@ -1126,6 +1100,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|> restrict_instance(opts) |> restrict_instance(opts)
|> restrict_announce_object_actor(opts) |> restrict_announce_object_actor(opts)
|> restrict_filtered(opts)
|> Activity.restrict_deactivated_users() |> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts) |> exclude_poll_votes(opts)
|> exclude_chat_messages(opts) |> exclude_chat_messages(opts)
@ -1251,6 +1226,8 @@ defp object_to_user_data(data) do
end) end)
locked = data["manuallyApprovesFollowers"] || false locked = data["manuallyApprovesFollowers"] || false
capabilities = data["capabilities"] || %{}
accepts_chat_messages = capabilities["acceptsChatMessages"]
data = Transmogrifier.maybe_fix_user_object(data) data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false discoverable = data["discoverable"] || false
invisible = data["invisible"] || false invisible = data["invisible"] || false
@ -1289,7 +1266,8 @@ defp object_to_user_data(data) do
also_known_as: Map.get(data, "alsoKnownAs", []), also_known_as: Map.get(data, "alsoKnownAs", []),
public_key: public_key, public_key: public_key,
inbox: data["inbox"], 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 # nickname can be nil because of virtual actors
@ -1398,6 +1376,31 @@ def fetch_and_prepare_user_from_ap_id(ap_id) do
end end
end end
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
def make_user_from_ap_id(ap_id) do def make_user_from_ap_id(ap_id) do
user = User.get_cached_by_ap_id(ap_id) user = User.get_cached_by_ap_id(ap_id)
@ -1410,6 +1413,8 @@ def make_user_from_ap_id(ap_id) do
|> User.remote_user_changeset(data) |> User.remote_user_changeset(data)
|> User.update_and_set_cache() |> User.update_and_set_cache()
else else
maybe_handle_clashing_nickname(data)
data data
|> User.remote_user_changeset() |> User.remote_user_changeset()
|> Repo.insert() |> Repo.insert()

View file

@ -14,6 +14,19 @@ defmodule Pleroma.Web.ActivityPub.Builder do
require Pleroma.Constants 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()} @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
def emoji_react(actor, object, emoji) do def emoji_react(actor, object, emoji) do
with {:ok, data, meta} <- object_action(actor, object) do with {:ok, data, meta} <- object_action(actor, object) do
@ -138,6 +151,18 @@ def update(actor, object) do
}, []} }, []}
end end
@spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
def block(blocker, blocked) do
{:ok,
%{
"id" => Utils.generate_activity_id(),
"type" => "Block",
"actor" => blocker.ap_id,
"object" => blocked.ap_id,
"to" => [blocked.ap_id]
}, []}
end
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()} @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
def announce(actor, object, options \\ []) do def announce(actor, object, options \\ []) do
public? = Keyword.get(options, :public, false) public? = Keyword.get(options, :public, false)

View file

@ -27,11 +27,14 @@ defp contains_links?(_), do: false
@impl true @impl true
def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do
with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor), with {:ok, %User{local: false} = u} <- User.get_or_fetch_by_ap_id(actor),
{:contains_links, true} <- {:contains_links, contains_links?(object)}, {:contains_links, true} <- {:contains_links, contains_links?(object)},
{:old_user, true} <- {:old_user, old_user?(u)} do {:old_user, true} <- {:old_user, old_user?(u)} do
{:ok, message} {:ok, message}
else else
{:ok, %User{local: true}} ->
{:ok, message}
{:contains_links, false} -> {:contains_links, false} ->
{:ok, message} {:ok, message}

View file

@ -98,7 +98,7 @@ def filter(message), do: {:ok, message}
@impl true @impl true
def describe do def describe do
mrf_object_age = mrf_object_age =
Pleroma.Config.get(:mrf_object_age) Config.get(:mrf_object_age)
|> Enum.into(%{}) |> Enum.into(%{})
{:ok, %{mrf_object_age: mrf_object_age}} {:ok, %{mrf_object_age: mrf_object_age}}

View file

@ -47,5 +47,5 @@ def filter(object), do: {:ok, object}
@impl true @impl true
def describe, def describe,
do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}} do: {:ok, %{mrf_rejectnonpublic: Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
end end

View file

@ -155,7 +155,7 @@ def filter(%{"type" => "Delete", "actor" => actor} = object) do
%{host: actor_host} = URI.parse(actor) %{host: actor_host} = URI.parse(actor)
reject_deletes = reject_deletes =
Pleroma.Config.get([:mrf_simple, :reject_deletes]) Config.get([:mrf_simple, :reject_deletes])
|> MRF.subdomains_regex() |> MRF.subdomains_regex()
if MRF.subdomain_match?(reject_deletes, actor_host) do if MRF.subdomain_match?(reject_deletes, actor_host) do

View file

@ -13,10 +13,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
@ -24,6 +26,35 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta) 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
|> BlockValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
block_activity = stringify_keys(block_activity)
outgoing_blocks = Pleroma.Config.get([:activitypub, :outgoing_blocks])
meta =
if !outgoing_blocks do
Keyword.put(meta, :do_not_federate, true)
else
meta
end
{:ok, block_activity, meta}
end
end
def validate(%{"type" => "Update"} = update_activity, meta) do def validate(%{"type" => "Update"} = update_activity, meta) do
with {:ok, update_activity} <- with {:ok, update_activity} <-
update_activity update_activity

View file

@ -0,0 +1,42 @@
# 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.BlockValidator 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)
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, ["Block"])
|> validate_actor_presence()
|> validate_actor_presence(field_name: :object)
end
def cast_and_validate(data) do
data
|> cast_data
|> validate_data
end
end

View file

@ -93,12 +93,14 @@ def validate_content_or_attachment(cng) do
- If both users are in our system - If both users are in our system
- If at least one of the users in this ChatMessage is a local user - 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 not blocking the actor
- If the recipient is explicitly not accepting chat messages
""" """
def validate_local_concern(cng) do def validate_local_concern(cng) do
with actor_ap <- get_field(cng, :actor), with actor_ap <- get_field(cng, :actor),
{_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)}, {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)},
{_, %User{} = recipient} <- {_, %User{} = recipient} <-
{:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())}, {: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)}, {_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)},
{_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do
cng cng
@ -107,6 +109,10 @@ def validate_local_concern(cng) do
cng cng
|> add_error(:actor, "actor is blocked by recipient") |> add_error(:actor, "actor is blocked by recipient")
{:not_accepting_chats?, true} ->
cng
|> add_error(:to, "recipient does not accept chat messages")
{:local?, false} -> {:local?, false} ->
cng cng
|> add_error(:actor, "actor and recipient are both remote") |> add_error(:actor, "actor and recipient are both remote")

View file

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

View file

@ -28,7 +28,7 @@ def relay_ap_id do
def follow(target_instance) do def follow(target_instance) do
with %User{} = local_user <- get_actor(), with %User{} = local_user <- get_actor(),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), {: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"]}") Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity} {:ok, activity}
else else

View file

@ -6,8 +6,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
collection, and so on. collection, and so on.
""" """
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.Chat.MessageReference alias Pleroma.Chat.MessageReference
alias Pleroma.FollowingRelationship
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
@ -20,6 +22,84 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
def handle(object, meta \\ []) 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(
%{data: %{"type" => "Block", "object" => blocked_user, "actor" => blocking_user}} =
object,
meta
) do
with %User{} = blocker <- User.get_cached_by_ap_id(blocking_user),
%User{} = blocked <- User.get_cached_by_ap_id(blocked_user) do
User.block(blocker, blocked)
end
{:ok, object, meta}
end
# Tasks this handles: # Tasks this handles:
# - Update the user # - Update the user
# #
@ -82,7 +162,10 @@ def handle(%{data: %{"type" => "Announce"}} = object, meta) do
if !User.is_internal_user?(user) do if !User.is_internal_user?(user) do
Notification.create_notifications(object) Notification.create_notifications(object)
ActivityPub.stream_out(object)
object
|> Topics.get_activity_topics()
|> Streamer.stream(object)
end end
{:ok, object, meta} {:ok, object, meta}
@ -190,14 +273,20 @@ def handle_object_creation(object) do
{:ok, object} {:ok, object}
end end
def handle_undoing(%{data: %{"type" => "Like"}} = object) do defp undo_like(nil, object), do: delete_object(object)
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_like_from_object(object, liked_object), defp undo_like(%Object{} = liked_object, object) do
{:ok, _} <- Repo.delete(object) do with {:ok, _} <- Utils.remove_like_from_object(object, liked_object) do
:ok delete_object(object)
end end
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 def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]), with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object), {:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object),
@ -227,6 +316,11 @@ def handle_undoing(
def handle_undoing(object), do: {:error, ["don't know how to handle", object]} 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 defp send_notifications(meta) do
Keyword.get(meta, :notifications, []) Keyword.get(meta, :notifications, [])
|> Enum.each(fn notification -> |> Enum.each(fn notification ->

View file

@ -233,8 +233,10 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
is_map(url) && is_binary(url["href"]) -> url["href"] is_map(url) && is_binary(url["href"]) -> url["href"]
is_binary(data["url"]) -> data["url"] is_binary(data["url"]) -> data["url"]
is_binary(data["href"]) -> data["href"] is_binary(data["href"]) -> data["href"]
true -> nil
end end
if href do
attachment_url = attachment_url =
%{"href" => href} %{"href" => href}
|> Maps.put_if_present("mediaType", media_type) |> Maps.put_if_present("mediaType", media_type)
@ -244,7 +246,11 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
|> Maps.put_if_present("mediaType", media_type) |> Maps.put_if_present("mediaType", media_type)
|> Maps.put_if_present("type", data["type"]) |> Maps.put_if_present("type", data["type"])
|> Maps.put_if_present("name", data["name"]) |> Maps.put_if_present("name", data["name"])
else
nil
end
end) end)
|> Enum.filter(& &1)
Map.put(object, "attachment", attachments) Map.put(object, "attachment", attachments)
end end
@ -263,12 +269,18 @@ def fix_url(%{"url" => url} = object) when is_map(url) do
def fix_url(%{"type" => object_type, "url" => url} = object) def fix_url(%{"type" => object_type, "url" => url} = object)
when object_type in ["Video", "Audio"] and is_list(url) do 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 object
|> Map.put("attachment", [first_element]) |> Map.put("attachment", [attachment])
|> Map.put("url", link_element["href"]) |> Map.put("url", link_element["href"])
end end
@ -446,12 +458,9 @@ def handle_incoming(
when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
actor = Containment.get_actor(data) actor = Containment.get_actor(data)
data =
Map.put(data, "actor", actor)
|> fix_addressing
with nil <- Activity.get_create_by_object_ap_id(object["id"]), with nil <- Activity.get_create_by_object_ap_id(object["id"]),
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor),
data <- Map.put(data, "actor", actor) |> fix_addressing() do
object = fix_object(object, options) object = fix_object(object, options)
params = %{ params = %{
@ -520,66 +529,6 @@ def handle_incoming(
end end
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( def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data, %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
_options _options
@ -673,7 +622,7 @@ def handle_incoming(
end end
def handle_incoming(%{"type" => type} = data, _options) def handle_incoming(%{"type" => type} = data, _options)
when type in ["Like", "EmojiReact", "Announce"] do when type in ~w{Like EmojiReact Announce} do
with :ok <- ObjectValidator.fetch_actor_and_object(data), with :ok <- ObjectValidator.fetch_actor_and_object(data),
{:ok, activity, _meta} <- {:ok, activity, _meta} <-
Pipeline.common_pipeline(data, local: false) do Pipeline.common_pipeline(data, local: false) do
@ -684,9 +633,10 @@ def handle_incoming(%{"type" => type} = data, _options)
end end
def handle_incoming( def handle_incoming(
%{"type" => "Update"} = data, %{"type" => type} = data,
_options _options
) do )
when type in ~w{Update Block Follow} do
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity} {:ok, activity}
@ -765,21 +715,6 @@ def handle_incoming(
end end
end end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
_options
) do
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming( def handle_incoming(
%{ %{
"type" => "Move", "type" => "Move",

View file

@ -81,6 +81,15 @@ def render("user.json", %{user: user}) do
fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue")) 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, "id" => user.ap_id,
"type" => user.actor_type, "type" => user.actor_type,
@ -101,7 +110,8 @@ def render("user.json", %{user: user}) do
"endpoints" => endpoints, "endpoints" => endpoints,
"attachment" => fields, "attachment" => fields,
"tag" => emoji_tags, "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.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))

View file

@ -47,6 +47,10 @@ def is_list?(_), do: false
@spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean() @spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean()
def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true
def visible_for_user?(nil, _), do: false
def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do
user.ap_id in activity.data["to"] || user.ap_id in activity.data["to"] ||
list_ap_id list_ap_id
@ -54,8 +58,6 @@ def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{
|> Pleroma.List.member?(user) |> Pleroma.List.member?(user)
end end
def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
def visible_for_user?(%{local: local} = activity, nil) do def visible_for_user?(%{local: local} = activity, nil) do
cfg_key = cfg_key =
if local, if local,

View file

@ -206,8 +206,8 @@ def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
end end
end end
def user_show(conn, %{"nickname" => nickname}) do def user_show(%{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 conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("show.json", %{user: user}) |> 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}) |> render("index.json", %{activities: activities, as: :activity})
end 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 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
godmode = params["godmode"] == "true" || params["godmode"] == 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) {_, page_size} = page_params(params)
activities = activities =
@ -526,7 +526,7 @@ def disable_mfa(conn, %{"nickname" => nickname}) do
@doc "Show a given user's credentials" @doc "Show a given user's credentials"
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do 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 conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("credentials.json", %{user: user, for: admin}) |> render("credentials.json", %{user: user, for: admin})

View file

@ -9,8 +9,6 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
alias Pleroma.ConfigDB alias Pleroma.ConfigDB
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
@descriptions Pleroma.Docs.JSON.compile()
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action == :update) 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 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation
def descriptions(conn, _params) do 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) json(conn, descriptions)
end end

View file

@ -40,7 +40,7 @@ def call(%{private: %{open_api_spex: private_data}} = conn, %{
|> List.first() |> List.first()
_ -> _ ->
nil "application/json"
end end
private_data = Map.put(private_data, :operation_id, operation_id) private_data = Map.put(private_data, :operation_id, operation_id)

View file

@ -61,7 +61,7 @@ def update_credentials_operation do
description: "Update the user's display and preferences.", description: "Update the user's display and preferences.",
operationId: "AccountController.update_credentials", operationId: "AccountController.update_credentials",
security: [%{"oAuth" => ["write:accounts"]}], security: [%{"oAuth" => ["write:accounts"]}],
requestBody: request_body("Parameters", update_creadentials_request(), required: true), requestBody: request_body("Parameters", update_credentials_request(), required: true),
responses: %{ responses: %{
200 => Operation.response("Account", "application/json", Account), 200 => Operation.response("Account", "application/json", Account),
403 => Operation.response("Error", "application/json", ApiError) 403 => Operation.response("Error", "application/json", ApiError)
@ -203,14 +203,23 @@ def follow_operation do
security: [%{"oAuth" => ["follow", "write:follows"]}], security: [%{"oAuth" => ["follow", "write:follows"]}],
description: "Follow the given account", description: "Follow the given account",
parameters: [ parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
Operation.parameter(
:reblogs,
:query,
BooleanLike,
"Receive this account's reblogs in home timeline? Defaults to true."
)
], ],
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: %{ responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship), 200 => Operation.response("Relationship", "application/json", AccountRelationship),
400 => Operation.response("Error", "application/json", ApiError), 400 => Operation.response("Error", "application/json", ApiError),
@ -438,6 +447,7 @@ defp create_request do
} }
end end
# TODO: This is actually a token respone, but there's no oauth operation file yet.
defp create_response do defp create_response do
%Schema{ %Schema{
title: "AccountCreateResponse", title: "AccountCreateResponse",
@ -446,19 +456,25 @@ defp create_response do
properties: %{ properties: %{
token_type: %Schema{type: :string}, token_type: %Schema{type: :string},
access_token: %Schema{type: :string}, access_token: %Schema{type: :string},
scope: %Schema{type: :array, items: %Schema{type: :string}}, refresh_token: %Schema{type: :string},
created_at: %Schema{type: :integer, format: :"date-time"} scope: %Schema{type: :string},
created_at: %Schema{type: :integer, format: :"date-time"},
me: %Schema{type: :string},
expires_in: %Schema{type: :integer}
}, },
example: %{ example: %{
"token_type" => "Bearer",
"access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk", "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
"refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz",
"created_at" => 1_585_918_714, "created_at" => 1_585_918_714,
"scope" => ["read", "write", "follow", "push"], "expires_in" => 600,
"token_type" => "Bearer" "scope" => "read write follow push",
"me" => "https://gensokyo.2hu/users/raymoo"
} }
} }
end end
defp update_creadentials_request do defp update_credentials_request do
%Schema{ %Schema{
title: "AccountUpdateCredentialsRequest", title: "AccountUpdateCredentialsRequest",
description: "POST body for creating an account", description: "POST body for creating an account",
@ -492,6 +508,11 @@ defp update_creadentials_request do
nullable: true, nullable: true,
description: "Whether manual approval of follow requests is required." 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{ fields_attributes: %Schema{
nullable: true, nullable: true,
oneOf: [ oneOf: [

View file

@ -4,7 +4,6 @@
defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.FlakeID
@ -40,48 +39,6 @@ def confirmation_resend_operation do
} }
end 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 def favourites_operation do
%Operation{ %Operation{
tags: ["Accounts"], tags: ["Accounts"],
@ -136,52 +93,4 @@ defp id_param do
required: true required: true
) )
end 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 end

View file

@ -84,7 +84,7 @@ def delete_operation do
operationId: "StatusController.delete", operationId: "StatusController.delete",
parameters: [id_param()], parameters: [id_param()],
responses: %{ responses: %{
200 => empty_object_response(), 200 => status_response(),
403 => Operation.response("Forbidden", "application/json", ApiError), 403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError) 404 => Operation.response("Not Found", "application/json", ApiError)
} }

View file

@ -40,20 +40,53 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
pleroma: %Schema{ pleroma: %Schema{
type: :object, type: :object,
properties: %{ properties: %{
allow_following_move: %Schema{type: :boolean}, allow_following_move: %Schema{
background_image: %Schema{type: :string, nullable: true}, type: :boolean,
description: "whether the user allows automatically follow moved following accounts"
},
background_image: %Schema{type: :string, nullable: true, format: :uri},
chat_token: %Schema{type: :string}, chat_token: %Schema{type: :string},
confirmation_pending: %Schema{type: :boolean}, confirmation_pending: %Schema{
type: :boolean,
description:
"whether the user account is waiting on email confirmation to be activated"
},
hide_favorites: %Schema{type: :boolean}, hide_favorites: %Schema{type: :boolean},
hide_followers_count: %Schema{type: :boolean}, hide_followers_count: %Schema{
hide_followers: %Schema{type: :boolean}, type: :boolean,
hide_follows_count: %Schema{type: :boolean}, description: "whether the user has follower stat hiding enabled"
hide_follows: %Schema{type: :boolean}, },
is_admin: %Schema{type: :boolean}, hide_followers: %Schema{
is_moderator: %Schema{type: :boolean}, type: :boolean,
description: "whether the user has follower hiding enabled"
},
hide_follows_count: %Schema{
type: :boolean,
description: "whether the user has follow stat hiding enabled"
},
hide_follows: %Schema{
type: :boolean,
description: "whether the user has follow hiding enabled"
},
is_admin: %Schema{
type: :boolean,
description: "whether the user is an admin of the local instance"
},
is_moderator: %Schema{
type: :boolean,
description: "whether the user is a moderator of the local instance"
},
skip_thread_containment: %Schema{type: :boolean}, skip_thread_containment: %Schema{type: :boolean},
tags: %Schema{type: :array, items: %Schema{type: :string}}, tags: %Schema{
unread_conversation_count: %Schema{type: :integer}, type: :array,
items: %Schema{type: :string},
description:
"List of tags being used for things like extra roles or moderation(ie. marking all media as nsfw all)."
},
unread_conversation_count: %Schema{
type: :integer,
description: "The count of unread conversations. Only returned to the account owner."
},
notification_settings: %Schema{ notification_settings: %Schema{
type: :object, type: :object,
properties: %{ properties: %{
@ -63,7 +96,16 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
}, },
relationship: AccountRelationship, relationship: AccountRelationship,
settings_store: %Schema{ settings_store: %Schema{
type: :object 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"
} }
} }
}, },
@ -71,16 +113,32 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
type: :object, type: :object,
properties: %{ properties: %{
fields: %Schema{type: :array, items: AccountField}, fields: %Schema{type: :array, items: AccountField},
note: %Schema{type: :string}, note: %Schema{
type: :string,
description:
"Plaintext version of the bio without formatting applied by the backend, used for editing the bio."
},
privacy: VisibilityScope, privacy: VisibilityScope,
sensitive: %Schema{type: :boolean}, sensitive: %Schema{type: :boolean},
pleroma: %Schema{ pleroma: %Schema{
type: :object, type: :object,
properties: %{ properties: %{
actor_type: ActorType, actor_type: ActorType,
discoverable: %Schema{type: :boolean}, discoverable: %Schema{
no_rich_text: %Schema{type: :boolean}, type: :boolean,
show_role: %Schema{type: :boolean} description:
"whether the user allows discovery of the account in search results and other services."
},
no_rich_text: %Schema{
type: :boolean,
description:
"whether the HTML tags for rich-text formatting are stripped from all statuses requested from the API."
},
show_role: %Schema{
type: :boolean,
description:
"whether the user wants their role (e.g admin, moderator) to be shown"
}
} }
} }
} }
@ -115,6 +173,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
"is_admin" => false, "is_admin" => false,
"is_moderator" => false, "is_moderator" => false,
"skip_thread_containment" => false, "skip_thread_containment" => false,
"accepts_chat_messages" => true,
"chat_token" => "chat_token" =>
"SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc", "SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc",
"unread_conversation_count" => 0, "unread_conversation_count" => 0,

View file

@ -62,6 +62,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
} }
}, },
content: %Schema{type: :string, format: :html, description: "HTML-encoded status content"}, 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{ created_at: %Schema{
type: :string, type: :string,
format: "date-time", format: "date-time",
@ -184,6 +189,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
thread_muted: %Schema{ thread_muted: %Schema{
type: :boolean, type: :boolean,
description: "`true` if the thread the post belongs to is muted" description: "`true` if the thread the post belongs to is muted"
},
parent_visible: %Schema{
type: :boolean,
description: "`true` if the parent post is visible to the user"
} }
} }
}, },

View file

@ -186,6 +186,7 @@ defp object(draft) do
draft.poll draft.poll
) )
|> Map.put("emoji", emoji) |> Map.put("emoji", emoji)
|> Map.put("source", draft.status)
%__MODULE__{draft | object: object} %__MODULE__{draft | object: object}
end end

View file

@ -25,6 +25,13 @@ defmodule Pleroma.Web.CommonAPI do
require Pleroma.Constants require Pleroma.Constants
require Logger require Logger
def block(blocker, blocked) do
with {:ok, block_data, _} <- Builder.block(blocker, blocked),
{:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
{:ok, block}
end
end
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
:ok <- validate_chat_content_length(content, !!maybe_attachment), :ok <- validate_chat_content_length(content, !!maybe_attachment),
@ -94,12 +101,16 @@ def unblock(blocker, blocked) do
def follow(follower, followed) do def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
with {:ok, follower} <- User.maybe_direct_follow(follower, followed), with {:ok, follow_data, _} <- Builder.follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed), {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
{:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
if activity.data["state"] == "reject" do
{:error, :rejected}
else
{:ok, follower, followed, activity} {:ok, follower, followed, activity}
end end
end end
end
def unfollow(follower, unfollowed) do def unfollow(follower, unfollowed) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),

View file

@ -143,7 +143,7 @@ def make_poll_data(%{"poll" => %{"expires_in" => expires_in}} = data)
def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data) def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)
when is_list(options) do when is_list(options) do
limits = Pleroma.Config.get([:instance, :poll_limits]) limits = Config.get([:instance, :poll_limits])
with :ok <- validate_poll_expiration(expires_in, limits), with :ok <- validate_poll_expiration(expires_in, limits),
:ok <- validate_poll_options_amount(options, limits), :ok <- validate_poll_options_amount(options, limits),
@ -502,7 +502,7 @@ def maybe_extract_mentions(_), do: []
def make_report_content_html(nil), do: {:ok, {nil, [], []}} def make_report_content_html(nil), do: {:ok, {nil, [], []}}
def make_report_content_html(comment) do def make_report_content_html(comment) do
max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000) max_size = Config.get([:instance, :max_report_comment_size], 1000)
if String.length(comment) <= max_size do if String.length(comment) <= max_size do
{:ok, format_input(comment, "text/plain")} {:ok, format_input(comment, "text/plain")}
@ -564,7 +564,7 @@ def validate_character_limit("" = _full_payload, [] = _attachments) do
end end
def validate_character_limit(full_payload, _attachments) do def validate_character_limit(full_payload, _attachments) do
limit = Pleroma.Config.get([:instance, :limit]) limit = Config.get([:instance, :limit])
length = String.length(full_payload) length = String.length(full_payload)
if length <= limit do if length <= limit do

View file

@ -27,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPIController alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
@ -101,12 +102,7 @@ def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
:ok <- TwitterAPI.validate_captcha(app, params), :ok <- TwitterAPI.validate_captcha(app, params),
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
json(conn, %{ json(conn, OAuthView.render("token.json", %{user: user, token: token}))
token_type: "Bearer",
access_token: token.token,
scope: app.scopes,
created_at: Token.Utils.format_created_at(token)
})
else else
{:error, error} -> json_response(conn, :bad_request, %{error: error}) {:error, error} -> json_response(conn, :bad_request, %{error: error})
end end
@ -148,6 +144,13 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|> Enum.filter(fn {_, value} -> not is_nil(value) end) |> Enum.filter(fn {_, value} -> not is_nil(value) end)
|> Enum.into(%{}) |> Enum.into(%{})
# We use an empty string as a special value to reset
# avatars, banners, backgrounds
user_image_value = fn
"" -> {:ok, nil}
value -> {:ok, value}
end
user_params = user_params =
[ [
:no_rich_text, :no_rich_text,
@ -160,7 +163,8 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
:show_role, :show_role,
:skip_thread_containment, :skip_thread_containment,
:allow_following_move, :allow_following_move,
:discoverable :discoverable,
:accepts_chat_messages
] ]
|> Enum.reduce(%{}, fn key, acc -> |> Enum.reduce(%{}, fn key, acc ->
Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)}) Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
@ -168,9 +172,9 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|> Maps.put_if_present(:name, params[:display_name]) |> Maps.put_if_present(:name, params[:display_name])
|> Maps.put_if_present(:bio, params[:note]) |> Maps.put_if_present(:bio, params[:note])
|> Maps.put_if_present(:raw_bio, params[:note]) |> Maps.put_if_present(:raw_bio, params[:note])
|> Maps.put_if_present(:avatar, params[:avatar]) |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
|> Maps.put_if_present(:banner, params[:header]) |> Maps.put_if_present(:banner, params[:header], user_image_value)
|> Maps.put_if_present(:background, params[:pleroma_background_image]) |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
|> Maps.put_if_present( |> Maps.put_if_present(
:raw_fields, :raw_fields,
params[:fields_attributes], params[:fields_attributes],
@ -346,7 +350,7 @@ def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not follow yourself"} {:error, "Can not follow yourself"}
end end
def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed) render(conn, "relationship.json", user: follower, target: followed)
else else
@ -385,8 +389,7 @@ def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
@doc "POST /api/v1/accounts/:id/block" @doc "POST /api/v1/accounts/:id/block"
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, _user_block} <- User.block(blocker, blocked), with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked) render(conn, "relationship.json", user: blocker, target: blocked)
else else
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :forbidden, %{error: message})

View file

@ -44,6 +44,7 @@ def search2(conn, params), do: do_search(:v2, conn, params)
def search(conn, params), do: do_search(:v1, conn, params) def search(conn, params), do: do_search(:v1, conn, params)
defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
query = String.trim(query)
options = search_options(params, user) options = search_options(params, user)
timeout = Keyword.get(Repo.config(), :timeout, 15_000) timeout = Keyword.get(Repo.config(), :timeout, 15_000)
default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}

View file

@ -200,11 +200,16 @@ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
@doc "DELETE /api/v1/statuses/:id" @doc "DELETE /api/v1/statuses/:id"
def delete(%{assigns: %{user: user}} = conn, %{id: id}) do def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do with %Activity{} = activity <- Activity.get_by_id_with_object(id),
json(conn, %{}) {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
try_render(conn, "show.json",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_source: true
)
else else
{:error, :not_found} = e -> e _e -> {:error, :not_found}
_e -> render_error(conn, :forbidden, "Can't delete this post")
end end
end end

View file

@ -88,21 +88,20 @@ def direct(%{assigns: %{user: user}} = conn, params) do
) )
end end
defp restrict_unauthenticated?(true = _local_only) do
Pleroma.Config.get([:restrict_unauthenticated, :timelines, :local])
end
defp restrict_unauthenticated?(_) do
Pleroma.Config.get([:restrict_unauthenticated, :timelines, :federated])
end
# GET /api/v1/timelines/public # GET /api/v1/timelines/public
def public(%{assigns: %{user: user}} = conn, params) do def public(%{assigns: %{user: user}} = conn, params) do
local_only = params[:local] local_only = params[:local]
cfg_key = if is_nil(user) and restrict_unauthenticated?(local_only) do
if local_only do fail_on_bad_auth(conn)
:local
else
:federated
end
restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key])
if restrict? and is_nil(user) do
render_error(conn, :unauthorized, "authorization required for timeline view")
else else
activities = activities =
params params
@ -123,6 +122,10 @@ def public(%{assigns: %{user: user}} = conn, params) do
end end
end end
defp fail_on_bad_auth(conn) do
render_error(conn, :unauthorized, "authorization required for timeline view")
end
defp hashtag_fetching(params, user, local_only) do defp hashtag_fetching(params, user, local_only) do
tags = tags =
[params[:tag], params[:any]] [params[:tag], params[:any]]
@ -157,6 +160,10 @@ defp hashtag_fetching(params, user, local_only) do
# GET /api/v1/timelines/tag/:tag # GET /api/v1/timelines/tag/:tag
def hashtag(%{assigns: %{user: user}} = conn, params) do def hashtag(%{assigns: %{user: user}} = conn, params) do
local_only = params[:local] local_only = params[:local]
if is_nil(user) and restrict_unauthenticated?(local_only) do
fail_on_bad_auth(conn)
else
activities = hashtag_fetching(params, user, local_only) activities = hashtag_fetching(params, user, local_only)
conn conn
@ -167,6 +174,7 @@ def hashtag(%{assigns: %{user: user}} = conn, params) do
as: :activity as: :activity
) )
end end
end
# GET /api/v1/timelines/list/:list_id # GET /api/v1/timelines/list/:list_id
def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do

View file

@ -204,6 +204,18 @@ defp do_render("show.json", %{user: user} = opts) do
%{} %{}
end end
favicon =
if Pleroma.Config.get([:instances_favicons, :enabled]) do
user
|> Map.get(:ap_id, "")
|> URI.parse()
|> URI.merge("/")
|> Pleroma.Instances.Instance.get_or_update_favicon()
|> MediaProxy.url()
else
nil
end
%{ %{
id: to_string(user.id), id: to_string(user.id),
username: username_from_nickname(user.nickname), username: username_from_nickname(user.nickname),
@ -245,7 +257,9 @@ defp do_render("show.json", %{user: user} = opts) do
hide_favorites: user.hide_favorites, hide_favorites: user.hide_favorites,
relationship: relationship, relationship: relationship,
skip_thread_containment: user.skip_thread_containment, skip_thread_containment: user.skip_thread_containment,
background_image: image_url(user.background) |> MediaProxy.url() background_image: image_url(user.background) |> MediaProxy.url(),
accepts_chat_messages: user.accepts_chat_messages,
favicon: favicon
} }
} }
|> maybe_put_role(user, opts[:for]) |> maybe_put_role(user, opts[:for])

View file

@ -34,10 +34,14 @@ def render("show.json", _) do
background_upload_limit: Keyword.get(instance, :background_upload_limit), background_upload_limit: Keyword.get(instance, :background_upload_limit),
banner_upload_limit: Keyword.get(instance, :banner_upload_limit), banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
background_image: Keyword.get(instance, :background_image), background_image: Keyword.get(instance, :background_image),
chat_limit: Keyword.get(instance, :chat_limit),
description_limit: Keyword.get(instance, :description_limit),
pleroma: %{ pleroma: %{
metadata: %{ metadata: %{
account_activation_required: Keyword.get(instance, :account_activation_required),
features: features(), features: features(),
federation: federation() federation: federation(),
fields_limits: fields_limits()
}, },
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
} }
@ -88,4 +92,13 @@ def federation do
end end
|> Map.put(:enabled, Config.get([:instance, :federating])) |> Map.put(:enabled, Config.get([:instance, :federating]))
end end
def fields_limits do
%{
max_fields: Config.get([:instance, :max_account_fields]),
max_remote_fields: Config.get([:instance, :max_remote_account_fields]),
name_length: Config.get([:instance, :account_field_name_length]),
value_length: Config.get([:instance, :account_field_value_length])
}
end
end end

View file

@ -21,7 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1] import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
# TODO: Add cached version. # TODO: Add cached version.
defp get_replied_to_activities([]), do: %{} defp get_replied_to_activities([]), do: %{}
@ -333,6 +333,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
reblog: nil, reblog: nil,
card: card, card: card,
content: content_html, content: content_html,
text: opts[:with_source] && object.data["source"],
created_at: created_at, created_at: created_at,
reblogs_count: announcement_count, reblogs_count: announcement_count,
replies_count: object.data["repliesCount"] || 0, replies_count: object.data["repliesCount"] || 0,
@ -364,7 +365,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
expires_at: expires_at, expires_at: expires_at,
direct_conversation_id: direct_conversation_id, direct_conversation_id: direct_conversation_id,
thread_muted: thread_muted?, thread_muted: thread_muted?,
emoji_reactions: emoji_reactions emoji_reactions: emoji_reactions,
parent_visible: visible_for_user?(reply_to, opts[:for])
} }
} }
end end

View file

@ -106,7 +106,7 @@ def filename(url_or_path) do
def build_url(sig_base64, url_base64, filename \\ nil) do def build_url(sig_base64, url_base64, filename \\ nil) do
[ [
Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()), Config.get([:media_proxy, :base_url], Web.base_url()),
"proxy", "proxy",
sig_base64, sig_base64,
url_base64, url_base64,

View file

@ -13,6 +13,7 @@ defmodule Pleroma.Web.OAuth.MFAController do
alias Pleroma.Web.Auth.TOTPAuthenticator alias Pleroma.Web.Auth.TOTPAuthenticator
alias Pleroma.Web.OAuth.MFAView, as: View alias Pleroma.Web.OAuth.MFAView, as: View
alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
plug(:fetch_session when action in [:show, :verify]) plug(:fetch_session when action in [:show, :verify])
@ -74,7 +75,7 @@ def challenge(conn, %{"mfa_token" => mfa_token} = params) do
{:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token),
{:ok, _} <- validates_challenge(user, params), {:ok, _} <- validates_challenge(user, params),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build(user, token)) json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else else
_error -> _error ->
conn conn

View file

@ -5,4 +5,13 @@
defmodule Pleroma.Web.OAuth.MFAView do defmodule Pleroma.Web.OAuth.MFAView do
use Pleroma.Web, :view use Pleroma.Web, :view
import Phoenix.HTML.Form import Phoenix.HTML.Form
alias Pleroma.MFA
def render("mfa_response.json", %{token: token, user: user}) do
%{
error: "mfa_required",
mfa_token: token.token,
supported_challenge_types: MFA.supported_methods(user)
}
end
end end

View file

@ -17,6 +17,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.MFAController alias Pleroma.Web.OAuth.MFAController
alias Pleroma.Web.OAuth.MFAView
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Scopes
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken
@ -233,9 +235,7 @@ def token_exchange(
with {:ok, app} <- Token.Utils.fetch_app(conn), with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token),
{:ok, token} <- RefreshToken.grant(token) do {:ok, token} <- RefreshToken.grant(token) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)} json(conn, OAuthView.render("token.json", %{user: user, token: token}))
json(conn, Token.Response.build(user, token, response_attrs))
else else
_error -> render_invalid_credentials_error(conn) _error -> render_invalid_credentials_error(conn)
end end
@ -247,9 +247,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"}
{:ok, auth} <- Authorization.get_by_token(app, fixed_token), {:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id), %User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
response_attrs = %{created_at: Token.Utils.format_created_at(token)} json(conn, OAuthView.render("token.json", %{user: user, token: token}))
json(conn, Token.Response.build(user, token, response_attrs))
else else
error -> error ->
handle_token_exchange_error(conn, error) handle_token_exchange_error(conn, error)
@ -267,7 +265,7 @@ def token_exchange(
{:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)},
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build(user, token)) json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else else
error -> error ->
handle_token_exchange_error(conn, error) handle_token_exchange_error(conn, error)
@ -290,7 +288,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
with {:ok, app} <- Token.Utils.fetch_app(conn), with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}), {:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build_for_client_credentials(token)) json(conn, OAuthView.render("token.json", %{token: token}))
else else
_error -> _error ->
handle_token_exchange_error(conn, :invalid_credentails) handle_token_exchange_error(conn, :invalid_credentails)
@ -548,7 +546,7 @@ defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
defp build_and_response_mfa_token(user, auth) do defp build_and_response_mfa_token(user, auth) do
with {:ok, token} <- MFA.Token.create_token(user, auth) do with {:ok, token} <- MFA.Token.create_token(user, auth) do
Token.Response.build_for_mfa_token(user, token) MFAView.render("mfa_response.json", %{token: token, user: user})
end end
end end

View file

@ -5,4 +5,26 @@
defmodule Pleroma.Web.OAuth.OAuthView do defmodule Pleroma.Web.OAuth.OAuthView do
use Pleroma.Web, :view use Pleroma.Web, :view
import Phoenix.HTML.Form import Phoenix.HTML.Form
alias Pleroma.Web.OAuth.Token.Utils
def render("token.json", %{token: token} = opts) do
response = %{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: expires_in(),
scope: Enum.join(token.scopes, " "),
created_at: Utils.format_created_at(token)
}
if user = opts[:user] do
response
|> Map.put(:me, user.ap_id)
else
response
end
end
defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end end

View file

@ -1,45 +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.Web.OAuth.Token.Response do
@moduledoc false
alias Pleroma.MFA
alias Pleroma.User
alias Pleroma.Web.OAuth.Token.Utils
@doc false
def build(%User{} = user, token, opts \\ %{}) do
%{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: expires_in(),
scope: Enum.join(token.scopes, " "),
me: user.ap_id
}
|> Map.merge(opts)
end
def build_for_client_credentials(token) do
%{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
created_at: Utils.format_created_at(token),
expires_in: expires_in(),
scope: Enum.join(token.scopes, " ")
}
end
def build_for_mfa_token(user, mfa_token) do
%{
error: "mfa_required",
mfa_token: mfa_token.token,
supported_challenge_types: MFA.supported_methods(user)
}
end
defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end

View file

@ -8,7 +8,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
import Pleroma.Web.ControllerHelper, import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
alias Ecto.Changeset
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
@ -35,17 +34,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
%{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe] %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
) )
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"]}
# Note: the following actions are not permission-secured in Mastodon:
when action in [
:update_avatar,
:update_banner,
:update_background
]
)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
@ -68,56 +56,6 @@ def confirmation_resend(conn, params) do
end end
end end
@doc "PATCH /api/v1/pleroma/accounts/update_avatar"
def update_avatar(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do
{:ok, _user} =
user
|> Changeset.change(%{avatar: nil})
|> User.update_and_set_cache()
json(conn, %{url: nil})
end
def update_avatar(%{assigns: %{user: user}, body_params: params} = conn, _params) do
{:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
{:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
%{"url" => [%{"href" => href} | _]} = data
json(conn, %{url: href})
end
@doc "PATCH /api/v1/pleroma/accounts/update_banner"
def update_banner(%{assigns: %{user: user}, body_params: %{banner: ""}} = conn, _) do
with {:ok, _user} <- User.update_banner(user, %{}) do
json(conn, %{url: nil})
end
end
def update_banner(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, object} <- ActivityPub.upload(%{img: params[:banner]}, type: :banner),
{:ok, _user} <- User.update_banner(user, object.data) do
%{"url" => [%{"href" => href} | _]} = object.data
json(conn, %{url: href})
end
end
@doc "PATCH /api/v1/pleroma/accounts/update_background"
def update_background(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do
with {:ok, _user} <- User.update_background(user, %{}) do
json(conn, %{url: nil})
end
end
def update_background(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, object} <- ActivityPub.upload(params, type: :background),
{:ok, _user} <- User.update_background(user, object.data) do
%{"url" => [%{"href" => href} | _]} = object.data
json(conn, %{url: href})
end
end
@doc "GET /api/v1/pleroma/accounts/:id/favourites" @doc "GET /api/v1/pleroma/accounts/:id/favourites"
def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do
render_error(conn, :forbidden, "Can't get favorites") render_error(conn, :forbidden, "Can't get favorites")

View file

@ -3,14 +3,15 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Preload.Providers.Instance do defmodule Pleroma.Web.Preload.Providers.Instance do
alias Pleroma.Plugs.InstanceStatic
alias Pleroma.Web.MastodonAPI.InstanceView alias Pleroma.Web.MastodonAPI.InstanceView
alias Pleroma.Web.Nodeinfo.Nodeinfo alias Pleroma.Web.Nodeinfo.Nodeinfo
alias Pleroma.Web.Preload.Providers.Provider alias Pleroma.Web.Preload.Providers.Provider
@behaviour Provider @behaviour Provider
@instance_url :"/api/v1/instance" @instance_url "/api/v1/instance"
@panel_url :"/instance/panel.html" @panel_url "/instance/panel.html"
@nodeinfo_url :"/nodeinfo/2.0" @nodeinfo_url "/nodeinfo/2.0.json"
@impl Provider @impl Provider
def generate_terms(_params) do def generate_terms(_params) do
@ -27,7 +28,7 @@ defp build_info_tag(acc) do
end end
defp build_panel_tag(acc) do defp build_panel_tag(acc) do
instance_path = Path.join(:code.priv_dir(:pleroma), "static/instance/panel.html") instance_path = InstanceStatic.file_path(@panel_url |> to_string())
if File.exists?(instance_path) do if File.exists?(instance_path) do
panel_data = File.read!(instance_path) panel_data = File.read!(instance_path)

View file

@ -1,24 +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.Web.Preload.Providers.StatusNet do
alias Pleroma.Web.Preload.Providers.Provider
alias Pleroma.Web.TwitterAPI.UtilView
@behaviour Provider
@config_url :"/api/statusnet/config.json"
@impl Provider
def generate_terms(_params) do
%{}
|> build_config_tag()
end
defp build_config_tag(acc) do
instance = Pleroma.Config.get(:instance)
info_data = UtilView.status_net_config(instance)
Map.put(acc, @config_url, info_data)
end
end

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Web.Preload.Providers.Timelines do
alias Pleroma.Web.Preload.Providers.Provider alias Pleroma.Web.Preload.Providers.Provider
@behaviour Provider @behaviour Provider
@public_url :"/api/v1/timelines/public" @public_url "/api/v1/timelines/public"
@impl Provider @impl Provider
def generate_terms(params) do def generate_terms(params) do

View file

@ -3,11 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Preload.Providers.User do defmodule Pleroma.Web.Preload.Providers.User do
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.Preload.Providers.Provider alias Pleroma.Web.Preload.Providers.Provider
@behaviour Provider @behaviour Provider
@account_url :"/api/v1/accounts" @account_url_base "/api/v1/accounts"
@impl Provider @impl Provider
def generate_terms(%{user: user}) do def generate_terms(%{user: user}) do
@ -16,10 +17,10 @@ def generate_terms(%{user: user}) do
def generate_terms(_params), do: %{} def generate_terms(_params), do: %{}
def build_accounts_tag(acc, nil), do: acc def build_accounts_tag(acc, %User{} = user) do
def build_accounts_tag(acc, user) do
account_data = AccountView.render("show.json", %{user: user, for: user}) account_data = AccountView.render("show.json", %{user: user, for: user})
Map.put(acc, @account_url, account_data) Map.put(acc, "#{@account_url_base}/#{user.id}", account_data)
end end
def build_accounts_tag(acc, _), do: acc
end end

View file

@ -86,7 +86,10 @@ defp parse_url(url) do
end end
try do try do
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: opts) rich_media_agent = Pleroma.Application.user_agent() <> "; Bot"
{:ok, %Tesla.Env{body: html}} =
Pleroma.HTTP.get(url, [{"user-agent", rich_media_agent}], adapter: opts)
html html
|> parse_html() |> parse_html()

View file

@ -328,10 +328,6 @@ defmodule Pleroma.Web.Router do
delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete) delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
post("/notifications/read", NotificationController, :mark_as_read) post("/notifications/read", NotificationController, :mark_as_read)
patch("/accounts/update_avatar", AccountController, :update_avatar)
patch("/accounts/update_banner", AccountController, :update_banner)
patch("/accounts/update_background", AccountController, :update_background)
get("/mascot", MascotController, :show) get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update) put("/mascot", MascotController, :update)
@ -516,10 +512,6 @@ defmodule Pleroma.Web.Router do
scope "/api", Pleroma.Web do scope "/api", Pleroma.Web do
pipe_through(:config) pipe_through(:config)
get("/help/test", TwitterAPI.UtilController, :help_test)
post("/help/test", TwitterAPI.UtilController, :help_test)
get("/statusnet/config", TwitterAPI.UtilController, :config)
get("/statusnet/version", TwitterAPI.UtilController, :version)
get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations) get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
end end

View file

@ -104,7 +104,9 @@ def stream(topics, items) do
:ok :ok
end end
def filtered_by_user?(%User{} = user, %Activity{} = item) do def filtered_by_user?(user, item, streamed_type \\ :activity)
def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
@ -116,6 +118,9 @@ def filtered_by_user?(%User{} = user, %Activity{} = item) do
true <- true <-
Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)), Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids, true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
true <-
!(streamed_type == :activity && item.data["type"] == "Announce" &&
parent.data["actor"] == user.ap_id),
true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)), true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),
true <- MapSet.disjoint?(recipients, recipient_blocks), true <- MapSet.disjoint?(recipients, recipient_blocks),
%{host: item_host} <- URI.parse(item.actor), %{host: item_host} <- URI.parse(item.actor),
@ -130,8 +135,8 @@ def filtered_by_user?(%User{} = user, %Activity{} = item) do
end end
end end
def filtered_by_user?(%User{} = user, %Notification{activity: activity}) do def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do
filtered_by_user?(user, activity) filtered_by_user?(user, activity, :notification)
end end
defp do_stream("direct", item) do defp do_stream("direct", item) do

View file

@ -13,9 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.TwitterAPI.UtilView
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe) plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe)
@ -42,12 +40,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version])
def help_test(conn, _params) do
json(conn, "ok")
end
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
with %User{} = user <- User.get_cached_by_nickname(nick), with %User{} = user <- User.get_cached_by_nickname(nick),
avatar = User.avatar_url(user) do avatar = User.avatar_url(user) do
@ -89,80 +81,14 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_
end end
end end
def config(%{assigns: %{format: "xml"}} = conn, _params) do
instance = Pleroma.Config.get(:instance)
response = UtilView.status_net_config(instance)
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, response)
end
def config(conn, _params) do
instance = Pleroma.Config.get(:instance)
vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
uploadlimit = %{
uploadlimit: to_string(Keyword.get(instance, :upload_limit)),
avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)),
backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)),
bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit))
}
data = %{
name: Keyword.get(instance, :name),
description: Keyword.get(instance, :description),
server: Web.base_url(),
textlimit: to_string(Keyword.get(instance, :limit)),
uploadlimit: uploadlimit,
closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"),
private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"),
vapidPublicKey: vapid_public_key,
accountActivationRequired:
bool_to_val(Keyword.get(instance, :account_activation_required, false)),
invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)),
safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions]))
}
managed_config = Keyword.get(instance, :managed_config)
data =
if managed_config do
pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
Map.put(data, "pleromafe", pleroma_fe)
else
data
end
json(conn, %{site: data})
end
defp bool_to_val(true), do: "1"
defp bool_to_val(_), do: "0"
defp bool_to_val(true, val, _), do: val
defp bool_to_val(_, _, val), do: val
def frontend_configurations(conn, _params) do def frontend_configurations(conn, _params) do
config = config =
Pleroma.Config.get(:frontend_configurations, %{}) Config.get(:frontend_configurations, %{})
|> Enum.into(%{}) |> Enum.into(%{})
json(conn, config) json(conn, config)
end end
def version(%{assigns: %{format: "xml"}} = conn, _params) do
version = Pleroma.Application.named_version()
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, "<version>#{version}</version>")
end
def version(conn, _params) do
json(conn, Pleroma.Application.named_version())
end
def emoji(conn, _params) do def emoji(conn, _params) do
emoji = emoji =
Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc -> Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->

View file

@ -86,7 +86,7 @@ def initial_state(token, user, custom_emojis) do
"video\/mp4" "video\/mp4"
] ]
}, },
settings: user.settings || @default_settings, settings: user.mastofe_settings || @default_settings,
push_subscription: nil, push_subscription: nil,
accounts: %{user.id => render(AccountView, "show.json", user: user, for: user)}, accounts: %{user.id => render(AccountView, "show.json", user: user, for: user)},
custom_emojis: render(CustomEmojiView, "index.json", custom_emojis: custom_emojis), custom_emojis: render(CustomEmojiView, "index.json", custom_emojis: custom_emojis),

View file

@ -11,13 +11,12 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do
use Pleroma.Workers.WorkerHelper, queue: "attachments_cleanup" use Pleroma.Workers.WorkerHelper, queue: "attachments_cleanup"
@impl Oban.Worker @impl Oban.Worker
def perform( def perform(%Job{
%{ args: %{
"op" => "cleanup_attachments", "op" => "cleanup_attachments",
"object" => %{"data" => %{"attachment" => [_ | _] = attachments, "actor" => actor}} "object" => %{"data" => %{"attachment" => [_ | _] = attachments, "actor" => actor}}
}, }
_job }) do
) do
attachments attachments
|> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end) |> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end)
|> fetch_objects |> fetch_objects
@ -28,7 +27,7 @@ def perform(
{:ok, :success} {:ok, :success}
end end
def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip} def perform(%Job{args: %{"op" => "cleanup_attachments", "object" => _object}}), do: {:ok, :skip}
defp do_clean({object_ids, attachment_urls}) do defp do_clean({object_ids, attachment_urls}) do
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])

View file

@ -11,59 +11,59 @@ defmodule Pleroma.Workers.BackgroundWorker do
@impl Oban.Worker @impl Oban.Worker
def perform(%{"op" => "deactivate_user", "user_id" => user_id, "status" => status}, _job) do def perform(%Job{args: %{"op" => "deactivate_user", "user_id" => user_id, "status" => status}}) do
user = User.get_cached_by_id(user_id) user = User.get_cached_by_id(user_id)
User.perform(:deactivate_async, user, status) User.perform(:deactivate_async, user, status)
end end
def perform(%{"op" => "delete_user", "user_id" => user_id}, _job) do def perform(%Job{args: %{"op" => "delete_user", "user_id" => user_id}}) do
user = User.get_cached_by_id(user_id) user = User.get_cached_by_id(user_id)
User.perform(:delete, user) User.perform(:delete, user)
end end
def perform(%{"op" => "force_password_reset", "user_id" => user_id}, _job) do def perform(%Job{args: %{"op" => "force_password_reset", "user_id" => user_id}}) do
user = User.get_cached_by_id(user_id) user = User.get_cached_by_id(user_id)
User.perform(:force_password_reset, user) User.perform(:force_password_reset, user)
end end
def perform( def perform(%Job{
%{ args: %{
"op" => "blocks_import", "op" => "blocks_import",
"blocker_id" => blocker_id, "blocker_id" => blocker_id,
"blocked_identifiers" => blocked_identifiers "blocked_identifiers" => blocked_identifiers
}, }
_job }) do
) do
blocker = User.get_cached_by_id(blocker_id) blocker = User.get_cached_by_id(blocker_id)
{:ok, User.perform(:blocks_import, blocker, blocked_identifiers)} {:ok, User.perform(:blocks_import, blocker, blocked_identifiers)}
end end
def perform( def perform(%Job{
%{ args: %{
"op" => "follow_import", "op" => "follow_import",
"follower_id" => follower_id, "follower_id" => follower_id,
"followed_identifiers" => followed_identifiers "followed_identifiers" => followed_identifiers
}, }
_job }) do
) do
follower = User.get_cached_by_id(follower_id) follower = User.get_cached_by_id(follower_id)
{:ok, User.perform(:follow_import, follower, followed_identifiers)} {:ok, User.perform(:follow_import, follower, followed_identifiers)}
end end
def perform(%{"op" => "media_proxy_preload", "message" => message}, _job) do def perform(%Job{args: %{"op" => "media_proxy_preload", "message" => message}}) do
MediaProxyWarmingPolicy.perform(:preload, message) MediaProxyWarmingPolicy.perform(:preload, message)
end end
def perform(%{"op" => "media_proxy_prefetch", "url" => url}, _job) do def perform(%Job{args: %{"op" => "media_proxy_prefetch", "url" => url}}) do
MediaProxyWarmingPolicy.perform(:prefetch, url) MediaProxyWarmingPolicy.perform(:prefetch, url)
end end
def perform(%{"op" => "fetch_data_for_activity", "activity_id" => activity_id}, _job) do def perform(%Job{args: %{"op" => "fetch_data_for_activity", "activity_id" => activity_id}}) do
activity = Activity.get_by_id(activity_id) activity = Activity.get_by_id(activity_id)
Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity) Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity)
end end
def perform(%{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}, _) do def perform(%Job{
args: %{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}
}) do
origin = User.get_cached_by_id(origin_id) origin = User.get_cached_by_id(origin_id)
target = User.get_cached_by_id(target_id) target = User.get_cached_by_id(target_id)

View file

@ -13,7 +13,7 @@ defmodule Pleroma.Workers.Cron.ClearOauthTokenWorker do
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
@impl Oban.Worker @impl Oban.Worker
def perform(_opts, _job) do def perform(_job) do
if Config.get([:oauth2, :clean_expired_tokens], false) do if Config.get([:oauth2, :clean_expired_tokens], false) do
Token.delete_expired_tokens() Token.delete_expired_tokens()
else else

View file

@ -19,7 +19,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do
require Logger require Logger
@impl Oban.Worker @impl Oban.Worker
def perform(_opts, _job) do def perform(_job) do
config = Config.get([:email_notifications, :digest]) config = Config.get([:email_notifications, :digest])
if config[:active] do if config[:active] do

View file

@ -12,7 +12,7 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorker do
use Pleroma.Workers.WorkerHelper, queue: "new_users_digest" use Pleroma.Workers.WorkerHelper, queue: "new_users_digest"
@impl Oban.Worker @impl Oban.Worker
def perform(_args, _job) do def perform(_job) do
if Pleroma.Config.get([Pleroma.Emails.NewUsersDigestEmail, :enabled]) do if Pleroma.Config.get([Pleroma.Emails.NewUsersDigestEmail, :enabled]) do
today = NaiveDateTime.utc_now() |> Timex.beginning_of_day() today = NaiveDateTime.utc_now() |> Timex.beginning_of_day()

View file

@ -20,7 +20,7 @@ defmodule Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker do
@interval :timer.minutes(1) @interval :timer.minutes(1)
@impl Oban.Worker @impl Oban.Worker
def perform(_opts, _job) do def perform(_job) do
if Config.get([ActivityExpiration, :enabled]) do if Config.get([ActivityExpiration, :enabled]) do
Enum.each(ActivityExpiration.due_expirations(@interval), &delete_activity/1) Enum.each(ActivityExpiration.due_expirations(@interval), &delete_activity/1)
else else

View file

@ -10,7 +10,7 @@ defmodule Pleroma.Workers.Cron.StatsWorker do
use Oban.Worker, queue: "background" use Oban.Worker, queue: "background"
@impl Oban.Worker @impl Oban.Worker
def perform(_opts, _job) do def perform(_job) do
Pleroma.Stats.do_collect() Pleroma.Stats.do_collect()
end end
end end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Workers.MailerWorker do
use Pleroma.Workers.WorkerHelper, queue: "mailer" use Pleroma.Workers.WorkerHelper, queue: "mailer"
@impl Oban.Worker @impl Oban.Worker
def perform(%{"op" => "email", "encoded_email" => encoded_email, "config" => config}, _job) do def perform(%Job{args: %{"op" => "email", "encoded_email" => encoded_email, "config" => config}}) do
encoded_email encoded_email
|> Base.decode64!() |> Base.decode64!()
|> :erlang.binary_to_term() |> :erlang.binary_to_term()

View file

@ -8,17 +8,17 @@ defmodule Pleroma.Workers.PublisherWorker do
use Pleroma.Workers.WorkerHelper, queue: "federator_outgoing" use Pleroma.Workers.WorkerHelper, queue: "federator_outgoing"
def backoff(attempt) when is_integer(attempt) do def backoff(%Job{attempt: attempt}) when is_integer(attempt) do
Pleroma.Workers.WorkerHelper.sidekiq_backoff(attempt, 5) Pleroma.Workers.WorkerHelper.sidekiq_backoff(attempt, 5)
end end
@impl Oban.Worker @impl Oban.Worker
def perform(%{"op" => "publish", "activity_id" => activity_id}, _job) do def perform(%Job{args: %{"op" => "publish", "activity_id" => activity_id}}) do
activity = Activity.get_by_id(activity_id) activity = Activity.get_by_id(activity_id)
Federator.perform(:publish, activity) Federator.perform(:publish, activity)
end end
def perform(%{"op" => "publish_one", "module" => module_name, "params" => params}, _job) do def perform(%Job{args: %{"op" => "publish_one", "module" => module_name, "params" => params}}) do
params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end) params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end)
Federator.perform(:publish_one, String.to_atom(module_name), params) Federator.perform(:publish_one, String.to_atom(module_name), params)
end end

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Workers.ReceiverWorker do
use Pleroma.Workers.WorkerHelper, queue: "federator_incoming" use Pleroma.Workers.WorkerHelper, queue: "federator_incoming"
@impl Oban.Worker @impl Oban.Worker
def perform(%{"op" => "incoming_ap_doc", "params" => params}, _job) do def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do
Federator.perform(:incoming_ap_doc, params) Federator.perform(:incoming_ap_doc, params)
end end
end end

View file

@ -8,13 +8,7 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do
use Pleroma.Workers.WorkerHelper, queue: "remote_fetcher" use Pleroma.Workers.WorkerHelper, queue: "remote_fetcher"
@impl Oban.Worker @impl Oban.Worker
def perform( def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do
%{
"op" => "fetch_remote",
"id" => id
} = args,
_job
) do
{:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"]) {:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"])
end end
end end

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