Fix merge conflicts with upstream

This commit is contained in:
Sean King 2021-06-04 14:42:44 -06:00
commit dc4814f0cd
No known key found for this signature in database
GPG key ID: 510C52BACD6E7257
240 changed files with 4616 additions and 1584 deletions

1
.gitignore vendored
View file

@ -28,6 +28,7 @@ erl_crash.dump
# variables. # variables.
/config/*.secret.exs /config/*.secret.exs
/config/generated_config.exs /config/generated_config.exs
/config/runtime.exs
/config/*.env /config/*.env

View file

@ -8,7 +8,9 @@ variables: &global_variables
MIX_ENV: test MIX_ENV: test
cache: &global_cache_policy cache: &global_cache_policy
key: ${CI_COMMIT_REF_SLUG} key:
files:
- mix.lock
paths: paths:
- deps - deps
- _build - _build
@ -22,6 +24,7 @@ stages:
- docker - docker
before_script: before_script:
- rm -rf _build/*/lib/pleroma
- apt-get update && apt-get install -y cmake - apt-get update && apt-get install -y cmake
- mix local.hex --force - mix local.hex --force
- mix local.rebar --force - mix local.rebar --force
@ -29,13 +32,25 @@ before_script:
- apt-get -qq update - apt-get -qq update
- apt-get install -y libmagic-dev - apt-get install -y libmagic-dev
after_script:
- rm -rf _build/*/lib/pleroma
build: build:
stage: build stage: build
only:
changes:
- "**/*.ex"
- "**/*.exs"
- "mix.lock"
script: script:
- mix compile --force - mix compile --force
spec-build: spec-build:
stage: test stage: test
only:
changes:
- "lib/pleroma/web/api_spec/**/*.ex"
- "lib/pleroma/web/api_spec.ex"
artifacts: artifacts:
paths: paths:
- spec.json - spec.json
@ -58,6 +73,11 @@ benchmark:
unit-testing: unit-testing:
stage: test stage: test
only:
changes:
- "**/*.ex"
- "**/*.exs"
- "mix.lock"
retry: 2 retry: 2
cache: &testing_cache_policy cache: &testing_cache_policy
<<: *global_cache_policy <<: *global_cache_policy
@ -91,6 +111,11 @@ unit-testing:
unit-testing-rum: unit-testing-rum:
stage: test stage: test
only:
changes:
- "**/*.ex"
- "**/*.exs"
- "mix.lock"
retry: 2 retry: 2
cache: *testing_cache_policy cache: *testing_cache_policy
services: services:
@ -109,12 +134,22 @@ unit-testing-rum:
lint: lint:
stage: test stage: test
only:
changes:
- "**/*.ex"
- "**/*.exs"
- "mix.lock"
cache: *testing_cache_policy cache: *testing_cache_policy
script: script:
- mix format --check-formatted - mix format --check-formatted
analysis: analysis:
stage: test stage: test
only:
changes:
- "**/*.ex"
- "**/*.exs"
- "mix.lock"
cache: *testing_cache_policy cache: *testing_cache_policy
script: script:
- mix credo --strict --only=warnings,todo,fixme,consistency,readability - mix credo --strict --only=warnings,todo,fixme,consistency,readability

View file

@ -12,11 +12,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed ### Changed
- **Breaking:** Configuration: `:chat, enabled` moved to `:shout, enabled` and `:instance, chat_limit` moved to `:shout, limit`
- The `application` metadata returned with statuses is no longer hardcoded. Apps that want to display these details will now have valid data for new posts after this change. - The `application` metadata returned with statuses is no longer hardcoded. Apps that want to display these details will now have valid data for new posts after this change.
- HTTPSecurityPlug now sends a response header to opt out of Google's FLoC (Federated Learning of Cohorts) targeted advertising.
- Email address is now returned if requesting user is the owner of the user account so it can be exposed in client and FE user settings UIs.
### Added ### Added
- MRF (`FollowBotPolicy`): New MRF Policy which makes a designated local Bot account attempt to follow all users in public Notes received by your instance. Users who require approving follower requests or have #nobot in their profile are excluded. - MRF (`FollowBotPolicy`): New MRF Policy which makes a designated local Bot account attempt to follow all users in public Notes received by your instance. Users who require approving follower requests or have #nobot in their profile are excluded.
- Return OAuth token `id` (primary key) in POST `/oauth/token`.
- `AnalyzeMetadata` upload filter for extracting attachment dimensions and generating blurhashes.
- Attachment dimensions and blurhashes are federated when available.
- Pinned posts federation
### Fixed
- Don't crash so hard when email settings are invalid.
- Checking activated Upload Filters for required commands.
### Removed
- **Breaking**: Remove deprecated `/api/qvitter/statuses/notifications/read` (replaced by `/api/v1/pleroma/notifications/read`)
## Unreleased (Patch) ## Unreleased (Patch)
@ -26,6 +40,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Uploading custom instance thumbnail via AdminAPI/AdminFE generated invalid URL to the image - Uploading custom instance thumbnail via AdminAPI/AdminFE generated invalid URL to the image
- Applying ConcurrentLimiter settings via AdminAPI - Applying ConcurrentLimiter settings via AdminAPI
- User login failures if their `notification_settings` were in a NULL state. - User login failures if their `notification_settings` were in a NULL state.
- Mix task `pleroma.user delete_activities` query transaction timeout is now :infinity
- MRF (`SimplePolicy`): Embedded objects are now checked. If any embedded object would be rejected, its parent is rejected. This fixes Announces leaking posts from blocked domains.
- Fixed some Markdown issues, including trailing slash in links.
## [2.3.0] - 2020-03-01 ## [2.3.0] - 2020-03-01

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 exiftool imagemagick libmagic ncurses postgresql-client &&\ apk add exiftool ffmpeg imagemagick libmagic 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

@ -50,5 +50,5 @@ If you are not developing Pleroma, it is better to use the OTP release, which co
- Latest Git revision: <https://docs-develop.pleroma.social> - Latest Git revision: <https://docs-develop.pleroma.social>
## Community Channels ## Community Channels
* IRC: **#pleroma** and **#pleroma-dev** on freenode, webchat is available at <https://irc.pleroma.social> * IRC: **#pleroma** and **#pleroma-dev** on libera.chat, webchat is available at <https://irc.pleroma.social>
* Matrix: <https://matrix.to/#/#freenode_#pleroma:matrix.org> and <https://matrix.to/#/#freenode_#pleroma-dev:matrix.org> * Matrix: [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) and [#pleroma-dev:libera.chat](https://matrix.to/#/#pleroma-dev:libera.chat)

View file

@ -299,7 +299,7 @@ defp insert_activity(:attachment, visibility, group, users, _opts) do
"url" => [ "url" => [
%{ %{
"href" => "href" =>
"#{Pleroma.Web.base_url()}/media/b1b873552422a07bf53af01f3c231c841db4dfc42c35efde681abaf0f2a4eab7.jpg", "#{Pleroma.Web.Endpoint.url()}/media/b1b873552422a07bf53af01f3c231c841db4dfc42c35efde681abaf0f2a4eab7.jpg",
"mediaType" => "image/jpeg", "mediaType" => "image/jpeg",
"type" => "Link" "type" => "Link"
} }

View file

@ -1,4 +1,4 @@
use Mix.Config import Config
# We don't run a server during test. If one is required, # We don't run a server during test. If one is required,
# you can enable the server option below. # you can enable the server option below.

View file

@ -41,7 +41,7 @@
# #
# This configuration file is loaded before any dependency and # This configuration file is loaded before any dependency and
# is restricted to this project. # is restricted to this project.
use Mix.Config import Config
# General application configuration # General application configuration
config :pleroma, ecto_repos: [Pleroma.Repo] config :pleroma, ecto_repos: [Pleroma.Repo]
@ -190,7 +190,6 @@
instance_thumbnail: "/instance/thumbnail.jpeg", instance_thumbnail: "/instance/thumbnail.jpeg",
limit: 5_000, limit: 5_000,
description_limit: 5_000, description_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,
avatar_upload_limit: 2_000_000, avatar_upload_limit: 2_000_000,
@ -454,7 +453,9 @@
image_quality: 85, image_quality: 85,
min_content_length: 100 * 1024 min_content_length: 100 * 1024
config :pleroma, :chat, enabled: true config :pleroma, :shout,
enabled: true,
limit: 5_000
config :phoenix, :format_encoders, json: Jason config :phoenix, :format_encoders, json: Jason

View file

@ -1,4 +1,4 @@
use Mix.Config import Config
websocket_config = [ websocket_config = [
path: "/websocket", path: "/websocket",
@ -544,14 +544,6 @@
5_000 5_000
] ]
}, },
%{
key: :chat_limit,
type: :integer,
description: "Character limit of the instance chat messages",
suggestions: [
5_000
]
},
%{ %{
key: :remote_limit, key: :remote_limit,
type: :integer, type: :integer,
@ -682,7 +674,8 @@
%{ %{
key: :allow_relay, key: :allow_relay,
type: :boolean, type: :boolean,
description: "Enable Pleroma's Relay, which makes it possible to follow a whole instance" description:
"Permits remote instances to subscribe to all public posts of your instance. (Important!) This may increase the visibility of your instance."
}, },
%{ %{
key: :public, key: :public,
@ -1182,7 +1175,6 @@
alwaysShowSubjectInput: true, alwaysShowSubjectInput: true,
background: "/static/aurora_borealis.jpg", background: "/static/aurora_borealis.jpg",
collapseMessageWithSubject: false, collapseMessageWithSubject: false,
disableChat: false,
greentext: false, greentext: false,
hideFilteredStatuses: false, hideFilteredStatuses: false,
hideMutedPosts: false, hideMutedPosts: false,
@ -1229,12 +1221,6 @@
description: description:
"When a message has a subject (aka Content Warning), collapse it by default" "When a message has a subject (aka Content Warning), collapse it by default"
}, },
%{
key: :disableChat,
label: "PleromaFE Chat",
type: :boolean,
description: "Disables PleromaFE Chat component"
},
%{ %{
key: :greentext, key: :greentext,
label: "Greentext", label: "Greentext",
@ -2633,13 +2619,22 @@
}, },
%{ %{
group: :pleroma, group: :pleroma,
key: :chat, key: :shout,
type: :group, type: :group,
description: "Pleroma chat settings", description: "Pleroma shout settings",
children: [ children: [
%{ %{
key: :enabled, key: :enabled,
type: :boolean type: :boolean,
description: "Enables the backend Shoutbox chat feature."
},
%{
key: :limit,
type: :integer,
description: "Shout message character limit.",
suggestions: [
5_000
]
} }
] ]
}, },

View file

@ -1,4 +1,4 @@
use Mix.Config import Config
# For development, we disable any cache and enable # For development, we disable any cache and enable
# debugging and code reloading. # debugging and code reloading.
@ -54,6 +54,10 @@
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
# Reduce recompilation time
# https://dashbit.co/blog/speeding-up-re-compilation-of-elixir-projects
config :phoenix, :plug_init_mode, :runtime
if File.exists?("./config/dev.secret.exs") do if File.exists?("./config/dev.secret.exs") do
import_config "dev.secret.exs" import_config "dev.secret.exs"
else else

View file

@ -1,4 +1,4 @@
use Mix.Config import Config
config :pleroma, Pleroma.Web.Endpoint, config :pleroma, Pleroma.Web.Endpoint,
http: [ http: [

View file

@ -1,4 +1,4 @@
use Mix.Config import Config
# For production, we often load configuration from external # For production, we often load configuration from external
# sources, such as your system environment. For this reason, # sources, such as your system environment. For this reason,
@ -63,7 +63,12 @@
# Finally import the config/prod.secret.exs # Finally import the config/prod.secret.exs
# which should be versioned separately. # which should be versioned separately.
import_config "prod.secret.exs" if File.exists?("./config/prod.secret.exs") do
import_config "prod.secret.exs"
else
"`config/prod.secret.exs` not found. You may want to create one by running `mix pleroma.instance gen`"
|> IO.warn([])
end
if File.exists?("./config/prod.exported_from_db.secret.exs"), if File.exists?("./config/prod.exported_from_db.secret.exs"),
do: import_config("prod.exported_from_db.secret.exs") do: import_config("prod.exported_from_db.secret.exs")

View file

@ -1,4 +1,4 @@
use Mix.Config import Config
# We don't run a server during test. If one is required, # We don't run a server during test. If one is required,
# you can enable the server option below. # you can enable the server option below.
@ -133,6 +133,10 @@
ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock, ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock,
logger: Pleroma.LoggerMock logger: Pleroma.LoggerMock
# Reduce recompilation time
# https://dashbit.co/blog/speeding-up-re-compilation-of-elixir-projects
config :phoenix, :plug_init_mode, :runtime
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

@ -8,9 +8,10 @@ For from source installations Pleroma configuration works by first importing the
To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted. To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted.
## :chat ## :shout
* `enabled` - Enables the backend chat. Defaults to `true`. * `enabled` - Enables the backend Shoutbox chat feature. Defaults to `true`.
* `limit` - Shout character limit. Defaults to `5_000`
## :instance ## :instance
* `name`: The instances name. * `name`: The instances name.
@ -19,7 +20,6 @@ To add configuration to your config file, you can copy it from the base config.
* `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).
* `description_limit`: The character limit for image descriptions. * `description_limit`: The character limit for image descriptions.
* `chat_limit`: Character limit of the instance chat messages.
* `remote_limit`: Hard character limit beyond which remote posts will be dropped. * `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).
* `avatar_upload_limit`: File size limit of users profile avatars. * `avatar_upload_limit`: File size limit of users profile avatars.
@ -37,7 +37,7 @@ To add configuration to your config file, you can copy it from the base config.
* `federating`: Enable federation with other instances. * `federating`: Enable federation with other instances.
* `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes. * `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
* `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it. * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
* `allow_relay`: Enable Pleromas Relay, which makes it possible to follow a whole instance. * `allow_relay`: Permits remote instances to subscribe to all public posts of your instance. This may increase the visibility of your 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. Note that there is a dependent setting restricting or allowing unauthenticated access to specific resources, see `restrict_unauthenticated` for more details. * `public`: Makes the client API in authenticated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network. Note that there is a dependent setting restricting or allowing unauthenticated access to specific resources, see `restrict_unauthenticated` for more details.
* `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.
* `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).

View file

@ -38,6 +38,7 @@ Has these additional fields under the `pleroma` object:
- `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. - `parent_visible`: If the parent of this post is visible to the user or not.
- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.
## Scheduled statuses ## Scheduled statuses
@ -255,9 +256,29 @@ This information is returned in the `/api/v1/accounts/verify_credentials` endpoi
*Pleroma supports refreshing tokens.* *Pleroma supports refreshing tokens.*
`POST /oauth/token` ### POST `/oauth/token`
Post here request with `grant_type=refresh_token` to obtain new access token. Returns an access token. You can obtain access tokens for a user in a few additional ways.
#### Refreshing a token
To obtain a new access token from a refresh token, pass `grant_type=refresh_token` with the following extra parameters:
- `refresh_token`: The refresh token.
#### Getting a token with a password
To obtain a token from a user's password, pass `grant_type=password` with the following extra parameters:
- `username`: Username to authenticate.
- `password`: The user's password.
#### Response body
Additional fields are returned in the response:
- `id`: The primary key of this token in Pleroma's database.
- `me` (user tokens only): The ActivityPub ID of the user who owns the token.
## Account Registration ## Account Registration

View file

@ -117,7 +117,7 @@ cd /opt/pleroma
sudo -Hu pleroma mix deps.get sudo -Hu pleroma mix deps.get
``` ```
* Generate the configuration: `sudo -Hu pleroma mix pleroma.instance gen` * Generate the configuration: `sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen`
* Answer with `yes` if it asks you to install `rebar3`. * Answer with `yes` if it asks you to install `rebar3`.
* This may take some time, because parts of pleroma get compiled first. * This may take some time, because parts of pleroma get compiled first.
* After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`. * After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`.
@ -240,4 +240,4 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
## Questions ## Questions
Questions about the installation or didnt it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. Questions about the installation or didnt it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC.

View file

@ -92,7 +92,7 @@ cd /opt/pleroma
sudo -Hu pleroma mix deps.get sudo -Hu pleroma mix deps.get
``` ```
* Generate the configuration: `sudo -Hu pleroma mix pleroma.instance gen` * Generate the configuration: `sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen`
* Answer with `yes` if it asks you to install `rebar3`. * Answer with `yes` if it asks you to install `rebar3`.
* This may take some time, because parts of pleroma get compiled first. * This may take some time, because parts of pleroma get compiled first.
* After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`. * After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`.
@ -215,4 +215,4 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
## Questions ## Questions
Questions about the installation or didnt it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. Questions about the installation or didnt it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC.

View file

@ -90,7 +90,7 @@ cd /opt/pleroma
sudo -Hu pleroma mix deps.get sudo -Hu pleroma mix deps.get
``` ```
* Generate the configuration: `sudo -Hu pleroma mix pleroma.instance gen` * Generate the configuration: `sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen`
* Answer with `yes` if it asks you to install `rebar3`. * Answer with `yes` if it asks you to install `rebar3`.
* This may take some time, because parts of pleroma get compiled first. * This may take some time, because parts of pleroma get compiled first.
* After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`. * After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`.
@ -202,4 +202,4 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
## Questions ## Questions
Questions about the installation or didnt it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. Questions about the installation or didnt it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC.

View file

@ -89,7 +89,7 @@ sudo -Hu pleroma mix deps.get
* コンフィギュレーションを生成します。 * コンフィギュレーションを生成します。
``` ```
sudo -Hu pleroma mix pleroma.instance gen sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen
``` ```
* rebar3をインストールしてもよいか聞かれたら、yesを入力してください。 * rebar3をインストールしてもよいか聞かれたら、yesを入力してください。
* このときにpleromaの一部がコンパイルされるため、この処理には時間がかかります。 * このときにpleromaの一部がコンパイルされるため、この処理には時間がかかります。
@ -103,7 +103,7 @@ sudo -Hu pleroma mv config/{generated_config.exs,prod.secret.exs}
* 先程のコマンドで、すでに `config/setup_db.psql` というファイルが作られています。このファイルをもとに、データベースを作成します。 * 先程のコマンドで、すでに `config/setup_db.psql` というファイルが作られています。このファイルをもとに、データベースを作成します。
``` ```
sudo -Hu pleroma mix pleroma.instance gen sudo -Hu pleroma MIX_ENV=prod mix pleroma.instance gen
``` ```
* そして、データベースのマイグレーションを実行します。 * そして、データベースのマイグレーションを実行します。
@ -191,5 +191,5 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
インストールについて質問がある、もしくは、うまくいかないときは、以下のところで質問できます。 インストールについて質問がある、もしくは、うまくいかないときは、以下のところで質問できます。
* [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) * [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat)
* **Freenode** の **#pleroma** IRCチャンネル * **libera.chat** の **#pleroma** IRCチャンネル

View file

@ -54,7 +54,7 @@ Configure Pleroma. Note that you need a domain name at this point:
``` ```
$ cd /home/pleroma/pleroma $ cd /home/pleroma/pleroma
$ mix deps.get # Enter "y" when asked to install Hex $ mix deps.get # Enter "y" when asked to install Hex
$ mix pleroma.instance gen # You will be asked a few questions here. $ MIX_ENV=prod mix pleroma.instance gen # You will be asked a few questions here.
$ cp config/generated_config.exs config/prod.secret.exs $ cp config/generated_config.exs config/prod.secret.exs
``` ```
@ -213,4 +213,4 @@ incorrect timestamps. You should have ntpd running.
## Questions ## Questions
Questions about the installation or didnt it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. Questions about the installation or didnt it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC.

View file

@ -135,7 +135,7 @@ pleroma$ mix deps.get
* Generate the configuration: * Generate the configuration:
```shell ```shell
pleroma$ mix pleroma.instance gen pleroma$ MIX_ENV=prod mix pleroma.instance gen
``` ```
* Answer with `yes` if it asks you to install `rebar3`. * Answer with `yes` if it asks you to install `rebar3`.
@ -298,4 +298,4 @@ If you opted to allow sudo for the `pleroma` user but would like to remove the a
## Questions ## Questions
Questions about the installation or didnt it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. Questions about the installation or didnt it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC.

View file

@ -71,7 +71,7 @@ Configure Pleroma. Note that you need a domain name at this point:
``` ```
$ cd /home/pleroma/pleroma $ cd /home/pleroma/pleroma
$ mix deps.get $ mix deps.get
$ mix pleroma.instance gen # You will be asked a few questions here. $ MIX_ENV=prod mix pleroma.instance gen # You will be asked a few questions here.
``` ```
Since Postgres is configured, we can now initialize the database. There should Since Postgres is configured, we can now initialize the database. There should
@ -193,8 +193,6 @@ Run `# /etc/rc.d/pleroma start` to start Pleroma.
Restart nginx with `# /etc/rc.d/nginx restart` and you should be up and running. Restart nginx with `# /etc/rc.d/nginx restart` and you should be up and running.
If you need further help, contact niaa on freenode.
Make sure your time is in sync, or other instances will receive your posts with Make sure your time is in sync, or other instances will receive your posts with
incorrect timestamps. You should have ntpd running. incorrect timestamps. You should have ntpd running.
@ -208,4 +206,4 @@ incorrect timestamps. You should have ntpd running.
## Questions ## Questions
Questions about the installation or didnt it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. Questions about the installation or didnt it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC.

View file

@ -239,7 +239,7 @@ Enter a shell as \_pleroma (as root `su _pleroma -`) and enter pleroma's install
Then follow the main installation guide: Then follow the main installation guide:
* run `mix deps.get` * run `mix deps.get`
* run `mix pleroma.instance gen` and enter your instance's information when asked * run `MIX_ENV=prod mix pleroma.instance gen` and enter your instance's information when asked
* copy config/generated\_config.exs to config/prod.secret.exs. The default values should be sufficient but you should edit it and check that everything seems OK. * copy config/generated\_config.exs to config/prod.secret.exs. The default values should be sufficient but you should edit it and check that everything seems OK.
* exit your current shell back to a root one and run `psql -U postgres -f /home/_pleroma/pleroma/config/setup_db.psql` to setup the database. * exit your current shell back to a root one and run `psql -U postgres -f /home/_pleroma/pleroma/config/setup_db.psql` to setup the database.
* return to a \_pleroma shell into pleroma's installation directory (`su _pleroma -;cd ~/pleroma`) and run `MIX_ENV=prod mix ecto.migrate` * return to a \_pleroma shell into pleroma's installation directory (`su _pleroma -;cd ~/pleroma`) and run `MIX_ENV=prod mix ecto.migrate`
@ -264,4 +264,4 @@ LC_ALL=en_US.UTF-8 MIX_ENV=prod mix pleroma.user new <username> <your@emailaddre
## Questions ## Questions
Questions about the installation or didnt it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. Questions about the installation or didnt it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC.

View file

@ -10,8 +10,8 @@ suositeltavaa tehdä komennon `doas` avulla, katso `doas (1)` ja `doas.conf (5)`
Tästä eteenpäin oletuksena on, että domain "esimerkki.com" osoittaa Tästä eteenpäin oletuksena on, että domain "esimerkki.com" osoittaa
serverin IP-osoitteeseen. serverin IP-osoitteeseen.
Jos asennuksen kanssa on ongelmia, IRC-kanava #pleroma Freenodessa tai Jos asennuksen kanssa on ongelmia, IRC-kanava #pleroma Libera.chat tai
Matrix-kanava #freenode_#pleroma:matrix.org ovat hyviä paikkoja löytää apua Matrix-kanava #pleroma:libera.chat ovat hyviä paikkoja löytää apua
(englanniksi), `/msg eal kukkuu` jos haluat välttämättä puhua härmää. (englanniksi), `/msg eal kukkuu` jos haluat välttämättä puhua härmää.
Asenna tarvittava ohjelmisto: Asenna tarvittava ohjelmisto:

View file

@ -232,7 +232,7 @@ At this point if you open your (sub)domain in a browser you should see a 502 err
If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errrors. If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errrors.
Still doesn't work? Feel free to contact us on [#pleroma on freenode](https://irc.pleroma.social) or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>, you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma-support/issues/new) Questions about the installation or didnt it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC, you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma-support/issues/new).
## Post installation ## Post installation
@ -290,7 +290,7 @@ nginx -t
## Create your first user and set as admin ## Create your first user and set as admin
```sh ```sh
cd /opt/pleroma/bin cd /opt/pleroma
su pleroma -s $SHELL -lc "./bin/pleroma_ctl user new joeuser joeuser@sld.tld --admin" su pleroma -s $SHELL -lc "./bin/pleroma_ctl user new joeuser joeuser@sld.tld --admin"
``` ```
This will create an account withe the username of 'joeuser' with the email address of joeuser@sld.tld, and set that user's account as an admin. This will result in a link that you can paste into the browser, which logs you in and enables you to set the password. This will create an account withe the username of 'joeuser' with the email address of joeuser@sld.tld, and set that user's account as an admin. This will result in a link that you can paste into the browser, which logs you in and enables you to set the password.
@ -301,4 +301,4 @@ This will create an account withe the username of 'joeuser' with the email addre
## Questions ## Questions
Questions about the installation or didnt it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. Questions about the installation or didnt it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC, you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma-support/issues/new).

View file

@ -184,40 +184,48 @@ def get_by_ap_id_with_object(ap_id) do
|> Repo.one() |> Repo.one()
end end
@spec get_by_id(String.t()) :: Activity.t() | nil @doc """
def get_by_id(id) do Gets activity by ID, doesn't load activities from deactivated actors by default.
case FlakeId.flake_id?(id) do """
true -> @spec get_by_id(String.t(), keyword()) :: t() | nil
Activity def get_by_id(id, opts \\ [filter: [:restrict_deactivated]]), do: get_by_id_with_opts(id, opts)
|> where([a], a.id == ^id)
|> restrict_deactivated_users()
|> Repo.one()
_ -> @spec get_by_id_with_user_actor(String.t()) :: t() | nil
nil def get_by_id_with_user_actor(id), do: get_by_id_with_opts(id, preload: [:user_actor])
@spec get_by_id_with_object(String.t()) :: t() | nil
def get_by_id_with_object(id), do: get_by_id_with_opts(id, preload: [:object])
defp get_by_id_with_opts(id, opts) do
if FlakeId.flake_id?(id) do
query = Queries.by_id(id)
with_filters_query =
if is_list(opts[:filter]) do
Enum.reduce(opts[:filter], query, fn
{:type, type}, acc -> Queries.by_type(acc, type)
:restrict_deactivated, acc -> restrict_deactivated_users(acc)
_, acc -> acc
end)
else
query
end
with_preloads_query =
if is_list(opts[:preload]) do
Enum.reduce(opts[:preload], with_filters_query, fn
:user_actor, acc -> with_preloaded_user_actor(acc)
:object, acc -> with_preloaded_object(acc)
_, acc -> acc
end)
else
with_filters_query
end
Repo.one(with_preloads_query)
end end
end end
def get_by_id_with_user_actor(id) do
case FlakeId.flake_id?(id) do
true ->
Activity
|> where([a], a.id == ^id)
|> with_preloaded_user_actor()
|> Repo.one()
_ ->
nil
end
end
def get_by_id_with_object(id) do
Activity
|> where(id: ^id)
|> with_preloaded_object()
|> Repo.one()
end
def all_by_ids_with_object(ids) do def all_by_ids_with_object(ids) do
Activity Activity
|> where([a], a.id in ^ids) |> where([a], a.id in ^ids)
@ -269,6 +277,11 @@ def get_create_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
def get_create_by_object_ap_id_with_object(_), do: nil def get_create_by_object_ap_id_with_object(_), do: nil
@spec create_by_id_with_object(String.t()) :: t() | nil
def create_by_id_with_object(id) do
get_by_id_with_opts(id, preload: [:object], filter: [type: "Create"])
end
defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}) do
get_create_by_object_ap_id_with_object(ap_id) get_create_by_object_ap_id_with_object(ap_id)
end end
@ -368,12 +381,6 @@ def direct_conversation_id(activity, for_user) do
end end
end end
@spec pinned_by_actor?(Activity.t()) :: boolean()
def pinned_by_actor?(%Activity{} = activity) do
actor = user_actor(activity)
activity.id in actor.pinned_activities
end
@spec get_by_object_ap_id_with_object(String.t()) :: t() | nil @spec get_by_object_ap_id_with_object(String.t()) :: t() | nil
def get_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do def get_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
ap_id ap_id
@ -384,4 +391,13 @@ def get_by_object_ap_id_with_object(ap_id) when is_binary(ap_id) do
end end
def get_by_object_ap_id_with_object(_), do: nil def get_by_object_ap_id_with_object(_), do: nil
@spec add_by_params_query(String.t(), String.t(), String.t()) :: Ecto.Query.t()
def add_by_params_query(object_id, actor, target) do
object_id
|> Queries.by_object_id()
|> Queries.by_type("Add")
|> Queries.by_actor(actor)
|> where([a], fragment("?->>'target' = ?", a.data, ^target))
end
end end

View file

@ -0,0 +1,45 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.HTML do
alias Pleroma.HTML
alias Pleroma.Object
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def get_cached_scrubbed_html_for_activity(
content,
scrubbers,
activity,
key \\ "",
callback \\ fn x -> x end
) do
key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
@cachex.fetch!(:scrubber_cache, key, fn _key ->
object = Object.normalize(activity, fetch: false)
HTML.ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
end)
end
def get_cached_stripped_html_for_activity(content, activity, key) do
get_cached_scrubbed_html_for_activity(
content,
FastSanitize.Sanitizer.StripTags,
activity,
key,
&HtmlEntities.decode/1
)
end
defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do
generate_scrubber_signature([scrubber])
end
defp generate_scrubber_signature(scrubbers) do
Enum.reduce(scrubbers, "", fn scrubber, signature ->
"#{signature}#{to_string(scrubber)}"
end)
end
end

View file

@ -14,6 +14,11 @@ defmodule Pleroma.Activity.Queries do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.User alias Pleroma.User
@spec by_id(query(), String.t()) :: query()
def by_id(query \\ Activity, id) do
from(a in query, where: a.id == ^id)
end
@spec by_ap_id(query, String.t()) :: query @spec by_ap_id(query, String.t()) :: query
def by_ap_id(query \\ Activity, ap_id) do def by_ap_id(query \\ Activity, ap_id) do
from( from(

View file

@ -25,7 +25,7 @@ def user_agent do
if Process.whereis(Pleroma.Web.Endpoint) do if Process.whereis(Pleroma.Web.Endpoint) do
case Config.get([:http, :user_agent], :default) do case Config.get([:http, :user_agent], :default) do
:default -> :default ->
info = "#{Pleroma.Web.base_url()} <#{Config.get([:instance, :email], "")}>" info = "#{Pleroma.Web.Endpoint.url()} <#{Config.get([:instance, :email], "")}>"
named_version() <> "; " <> info named_version() <> "; " <> info
custom -> custom ->
@ -102,7 +102,7 @@ def start(_type, _args) do
] ++ ] ++
task_children(@mix_env) ++ task_children(@mix_env) ++
dont_run_in_test(@mix_env) ++ dont_run_in_test(@mix_env) ++
chat_child(chat_enabled?()) ++ shout_child(shout_enabled?()) ++
[Pleroma.Gopher.Server] [Pleroma.Gopher.Server]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
@ -216,7 +216,7 @@ def build_cachex(type, opts),
type: :worker type: :worker
} }
defp chat_enabled?, do: Config.get([:chat, :enabled]) defp shout_enabled?, do: Config.get([:shout, :enabled])
defp dont_run_in_test(env) when env in [:test, :benchmark], do: [] defp dont_run_in_test(env) when env in [:test, :benchmark], do: []
@ -237,14 +237,14 @@ defp background_migrators do
] ]
end end
defp chat_child(true) do defp shout_child(true) do
[ [
Pleroma.Web.ChatChannel.ChatChannelState, Pleroma.Web.ShoutChannel.ShoutChannelState,
{Phoenix.PubSub, [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]} {Phoenix.PubSub, [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]}
] ]
end end
defp chat_child(_), do: [] defp shout_child(_), do: []
defp task_children(:test) do defp task_children(:test) do
[ [

View file

@ -34,15 +34,16 @@ defp handle_result({:error, message}), do: raise(VerifyError, message: message)
defp check_welcome_message_config!(:ok) do defp check_welcome_message_config!(:ok) do
if Pleroma.Config.get([:welcome, :email, :enabled], false) and if Pleroma.Config.get([:welcome, :email, :enabled], false) and
not Pleroma.Emails.Mailer.enabled?() do not Pleroma.Emails.Mailer.enabled?() do
Logger.error(""" Logger.warn("""
To send welcome email do you need to enable mail. To send welcome emails, you need to enable the mailer.
\nconfig :pleroma, Pleroma.Emails.Mailer, enabled: true Welcome emails will NOT be sent with the current config.
""")
{:error, "The mail disabled."} Enable the mailer:
else config :pleroma, Pleroma.Emails.Mailer, enabled: true
:ok """)
end end
:ok
end end
defp check_welcome_message_config!(result), do: result defp check_welcome_message_config!(result), do: result
@ -51,18 +52,21 @@ defp check_welcome_message_config!(result), do: result
# #
def check_confirmation_accounts!(:ok) do def check_confirmation_accounts!(:ok) do
if Pleroma.Config.get([:instance, :account_activation_required]) && if Pleroma.Config.get([:instance, :account_activation_required]) &&
not Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do not Pleroma.Emails.Mailer.enabled?() do
Logger.error( Logger.warn("""
"Account activation enabled, but no Mailer settings enabled.\n" <> Account activation is required, but the mailer is disabled.
"Please set config :pleroma, :instance, account_activation_required: false\n" <> Users will NOT be able to confirm their accounts with this config.
"Otherwise setup and enable Mailer." Either disable account activation or enable the mailer.
)
{:error, Disable account activation:
"Account activation enabled, but Mailer is disabled. Cannot send confirmation emails."} config :pleroma, :instance, account_activation_required: false
else
:ok Enable the mailer:
config :pleroma, Pleroma.Emails.Mailer, enabled: true
""")
end end
:ok
end end
def check_confirmation_accounts!(result), do: result def check_confirmation_accounts!(result), do: result
@ -160,9 +164,11 @@ defp do_check_rum!(setting, migrate) do
defp check_system_commands!(:ok) do defp check_system_commands!(:ok) do
filter_commands_statuses = [ filter_commands_statuses = [
check_filter(Pleroma.Upload.Filters.Exiftool, "exiftool"), check_filter(Pleroma.Upload.Filter.Exiftool, "exiftool"),
check_filter(Pleroma.Upload.Filters.Mogrify, "mogrify"), check_filter(Pleroma.Upload.Filter.Mogrify, "mogrify"),
check_filter(Pleroma.Upload.Filters.Mogrifun, "mogrify") check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "mogrify"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "convert")
] ]
preview_proxy_commands_status = preview_proxy_commands_status =

View file

@ -41,7 +41,8 @@ def warn do
:ok <- check_gun_pool_options(), :ok <- check_gun_pool_options(),
:ok <- check_activity_expiration_config(), :ok <- check_activity_expiration_config(),
:ok <- check_remote_ip_plug_name(), :ok <- check_remote_ip_plug_name(),
:ok <- check_uploders_s3_public_endpoint() do :ok <- check_uploders_s3_public_endpoint(),
:ok <- check_old_chat_shoutbox() do
:ok :ok
else else
_ -> _ ->
@ -215,4 +216,27 @@ def check_uploders_s3_public_endpoint do
:ok :ok
end end
end end
@spec check_old_chat_shoutbox() :: :ok | nil
def check_old_chat_shoutbox do
instance_config = Pleroma.Config.get([:instance])
chat_config = Pleroma.Config.get([:chat]) || []
use_old_config =
Keyword.has_key?(instance_config, :chat_limit) or
Keyword.has_key?(chat_config, :enabled)
if use_old_config do
Logger.error("""
!!!DEPRECATION WARNING!!!
Your config is using the old namespace for the Shoutbox configuration. You need to convert to the new namespace. e.g.,
\n* `config :pleroma, :chat, enabled` and `config :pleroma, :instance, chat_limit` are now equal to:
\n* `config :pleroma, :shout, enabled` and `config :pleroma, :shout, limit`
""")
:error
else
:ok
end
end
end end

View file

@ -3,19 +3,21 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Loader do defmodule Pleroma.Config.Loader do
@reject_keys [ defp reject_keys,
Pleroma.Repo, do: [
Pleroma.Web.Endpoint, Pleroma.Repo,
:env, Pleroma.Web.Endpoint,
:configurable_from_database, :env,
:database, :configurable_from_database,
:swarm :database,
] :swarm
]
@reject_groups [ defp reject_groups,
:postgrex, do: [
:tesla :postgrex,
] :tesla
]
if Code.ensure_loaded?(Config.Reader) do if Code.ensure_loaded?(Config.Reader) do
@reader Config.Reader @reader Config.Reader
@ -52,7 +54,7 @@ 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 in @reject_groups or key in reject_keys() or group in reject_groups() or
(group == :phoenix and key == :serve_endpoints) (group == :phoenix and key == :serve_endpoints)
end) end)
end end

View file

@ -1,6 +1,6 @@
defmodule Pleroma.Config.ReleaseRuntimeProvider do defmodule Pleroma.Config.ReleaseRuntimeProvider do
@moduledoc """ @moduledoc """
Imports `runtime.exs` and `{env}.exported_from_db.secret.exs` for elixir releases. Imports runtime config and `{env}.exported_from_db.secret.exs` for releases.
""" """
@behaviour Config.Provider @behaviour Config.Provider
@ -8,10 +8,11 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do
def init(opts), do: opts def init(opts), do: opts
@impl true @impl true
def load(config, _opts) do def load(config, opts) do
with_defaults = Config.Reader.merge(config, Pleroma.Config.Holder.release_defaults()) with_defaults = Config.Reader.merge(config, Pleroma.Config.Holder.release_defaults())
config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs" config_path =
opts[:config_path] || System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"
with_runtime_config = with_runtime_config =
if File.exists?(config_path) do if File.exists?(config_path) do
@ -24,7 +25,7 @@ def load(config, _opts) do
warning = [ warning = [
IO.ANSI.red(), IO.ANSI.red(),
IO.ANSI.bright(), IO.ANSI.bright(),
"!!! #{config_path} not found! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file", "!!! Config path is not declared! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file",
IO.ANSI.reset() IO.ANSI.reset()
] ]
@ -33,13 +34,14 @@ def load(config, _opts) do
end end
exported_config_path = exported_config_path =
config_path opts[:exported_config_path] ||
|> Path.dirname() config_path
|> Path.join("prod.exported_from_db.secret.exs") |> Path.dirname()
|> Path.join("#{Pleroma.Config.get(:env)}.exported_from_db.secret.exs")
with_exported = with_exported =
if File.exists?(exported_config_path) do if File.exists?(exported_config_path) do
exported_config = Config.Reader.read!(with_runtime_config) exported_config = Config.Reader.read!(exported_config_path)
Config.Reader.merge(with_runtime_config, exported_config) Config.Reader.merge(with_runtime_config, exported_config)
else else
with_runtime_config with_runtime_config

View file

@ -13,23 +13,25 @@ defmodule Pleroma.Config.TransferTask do
@type env() :: :test | :benchmark | :dev | :prod @type env() :: :test | :benchmark | :dev | :prod
@reboot_time_keys [ defp reboot_time_keys,
{:pleroma, :hackney_pools}, do: [
{:pleroma, :chat}, {:pleroma, :hackney_pools},
{:pleroma, Oban}, {:pleroma, :shout},
{:pleroma, :rate_limit}, {:pleroma, Oban},
{:pleroma, :markup}, {:pleroma, :rate_limit},
{:pleroma, :streamer}, {:pleroma, :markup},
{:pleroma, :pools}, {:pleroma, :streamer},
{:pleroma, :connections_pool} {:pleroma, :pools},
] {:pleroma, :connections_pool}
]
@reboot_time_subkeys [ defp reboot_time_subkeys,
{:pleroma, Pleroma.Captcha, [:seconds_valid]}, do: [
{:pleroma, Pleroma.Upload, [:proxy_remote]}, {:pleroma, Pleroma.Captcha, [:seconds_valid]},
{:pleroma, :instance, [:upload_limit]}, {:pleroma, Pleroma.Upload, [:proxy_remote]},
{:pleroma, :gopher, [:enabled]} {:pleroma, :instance, [:upload_limit]},
] {:pleroma, :gopher, [:enabled]}
]
def start_link(restart_pleroma? \\ true) do def start_link(restart_pleroma? \\ true) do
load_and_update_env([], restart_pleroma?) load_and_update_env([], restart_pleroma?)
@ -165,12 +167,12 @@ def pleroma_need_restart?(group, key, value) do
end end
defp group_and_key_need_reboot?(group, key) do defp group_and_key_need_reboot?(group, key) do
Enum.any?(@reboot_time_keys, fn {g, k} -> g == group and k == key end) Enum.any?(reboot_time_keys(), fn {g, k} -> g == group and k == key end)
end end
defp group_and_subkey_need_reboot?(group, key, value) do defp group_and_subkey_need_reboot?(group, key, value) do
Keyword.keyword?(value) and Keyword.keyword?(value) and
Enum.any?(@reboot_time_subkeys, fn {g, k, subkeys} -> Enum.any?(reboot_time_subkeys(), fn {g, k, subkeys} ->
g == group and k == key and g == group and k == key and
Enum.any?(Keyword.keys(value), &(&1 in subkeys)) Enum.any?(Keyword.keys(value), &(&1 in subkeys))
end) end)

View file

@ -27,6 +27,4 @@ defmodule Pleroma.Constants do
do: do:
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css) ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
) )
def as_local_public, do: Pleroma.Web.base_url() <> "/#Public"
end end

View file

@ -1,256 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
#
# This file is derived from Earmark, under the following copyright:
# Copyright © 2014 Dave Thomas, The Pragmatic Programmers
# SPDX-License-Identifier: Apache-2.0
# Upstream: https://github.com/pragdave/earmark/blob/master/lib/earmark/html_renderer.ex
defmodule Pleroma.EarmarkRenderer do
@moduledoc false
alias Earmark.Block
alias Earmark.Context
alias Earmark.HtmlRenderer
alias Earmark.Options
import Earmark.Inline, only: [convert: 3]
import Earmark.Helpers.HtmlHelpers
import Earmark.Message, only: [add_messages_from: 2, get_messages: 1, set_messages: 2]
import Earmark.Context, only: [append: 2, set_value: 2]
import Earmark.Options, only: [get_mapper: 1]
@doc false
def render(blocks, %Context{options: %Options{}} = context) do
messages = get_messages(context)
{contexts, html} =
get_mapper(context.options).(
blocks,
&render_block(&1, put_in(context.options.messages, []))
)
|> Enum.unzip()
all_messages =
contexts
|> Enum.reduce(messages, fn ctx, messages1 -> messages1 ++ get_messages(ctx) end)
{put_in(context.options.messages, all_messages), html |> IO.iodata_to_binary()}
end
#############
# Paragraph #
#############
defp render_block(%Block.Para{lnb: lnb, lines: lines, attrs: attrs}, context) do
lines = convert(lines, lnb, context)
add_attrs(lines, "<p>#{lines.value}</p>", attrs, [], lnb)
end
########
# Html #
########
defp render_block(%Block.Html{html: html}, context) do
{context, html}
end
defp render_block(%Block.HtmlComment{lines: lines}, context) do
{context, lines}
end
defp render_block(%Block.HtmlOneline{html: html}, context) do
{context, html}
end
#########
# Ruler #
#########
defp render_block(%Block.Ruler{lnb: lnb, attrs: attrs}, context) do
add_attrs(context, "<hr />", attrs, [], lnb)
end
###########
# Heading #
###########
defp render_block(
%Block.Heading{lnb: lnb, level: level, content: content, attrs: attrs},
context
) do
converted = convert(content, lnb, context)
html = "<h#{level}>#{converted.value}</h#{level}>"
add_attrs(converted, html, attrs, [], lnb)
end
##############
# Blockquote #
##############
defp render_block(%Block.BlockQuote{lnb: lnb, blocks: blocks, attrs: attrs}, context) do
{context1, body} = render(blocks, context)
html = "<blockquote>#{body}</blockquote>"
add_attrs(context1, html, attrs, [], lnb)
end
#########
# Table #
#########
defp render_block(
%Block.Table{lnb: lnb, header: header, rows: rows, alignments: aligns, attrs: attrs},
context
) do
{context1, html} = add_attrs(context, "<table>", attrs, [], lnb)
context2 = set_value(context1, html)
context3 =
if header do
append(add_trs(append(context2, "<thead>"), [header], "th", aligns, lnb), "</thead>")
else
# Maybe an error, needed append(context, html)
context2
end
context4 = append(add_trs(append(context3, "<tbody>"), rows, "td", aligns, lnb), "</tbody>")
{context4, [context4.value, "</table>"]}
end
########
# Code #
########
defp render_block(
%Block.Code{lnb: lnb, language: language, attrs: attrs} = block,
%Context{options: options} = context
) do
class =
if language, do: ~s{ class="#{code_classes(language, options.code_class_prefix)}"}, else: ""
tag = ~s[<pre><code#{class}>]
lines = options.render_code.(block)
html = ~s[#{tag}#{lines}</code></pre>]
add_attrs(context, html, attrs, [], lnb)
end
#########
# Lists #
#########
defp render_block(
%Block.List{lnb: lnb, type: type, blocks: items, attrs: attrs, start: start},
context
) do
{context1, content} = render(items, context)
html = "<#{type}#{start}>#{content}</#{type}>"
add_attrs(context1, html, attrs, [], lnb)
end
# format a single paragraph list item, and remove the para tags
defp render_block(
%Block.ListItem{lnb: lnb, blocks: blocks, spaced: false, attrs: attrs},
context
)
when length(blocks) == 1 do
{context1, content} = render(blocks, context)
content = Regex.replace(~r{</?p>}, content, "")
html = "<li>#{content}</li>"
add_attrs(context1, html, attrs, [], lnb)
end
# format a spaced list item
defp render_block(%Block.ListItem{lnb: lnb, blocks: blocks, attrs: attrs}, context) do
{context1, content} = render(blocks, context)
html = "<li>#{content}</li>"
add_attrs(context1, html, attrs, [], lnb)
end
##################
# Footnote Block #
##################
defp render_block(%Block.FnList{blocks: footnotes}, context) do
items =
Enum.map(footnotes, fn note ->
blocks = append_footnote_link(note)
%Block.ListItem{attrs: "#fn:#{note.number}", type: :ol, blocks: blocks}
end)
{context1, html} = render_block(%Block.List{type: :ol, blocks: items}, context)
{context1, Enum.join([~s[<div class="footnotes">], "<hr />", html, "</div>"])}
end
#######################################
# Isolated IALs are rendered as paras #
#######################################
defp render_block(%Block.Ial{verbatim: verbatim}, context) do
{context, "<p>{:#{verbatim}}</p>"}
end
####################
# IDDef is ignored #
####################
defp render_block(%Block.IdDef{}, context), do: {context, ""}
#####################################
# And here are the inline renderers #
#####################################
defdelegate br, to: HtmlRenderer
defdelegate codespan(text), to: HtmlRenderer
defdelegate em(text), to: HtmlRenderer
defdelegate strong(text), to: HtmlRenderer
defdelegate strikethrough(text), to: HtmlRenderer
defdelegate link(url, text), to: HtmlRenderer
defdelegate link(url, text, title), to: HtmlRenderer
defdelegate image(path, alt, title), to: HtmlRenderer
defdelegate footnote_link(ref, backref, number), to: HtmlRenderer
# Table rows
defp add_trs(context, rows, tag, aligns, lnb) do
numbered_rows =
rows
|> Enum.zip(Stream.iterate(lnb, &(&1 + 1)))
numbered_rows
|> Enum.reduce(context, fn {row, lnb}, ctx ->
append(add_tds(append(ctx, "<tr>"), row, tag, aligns, lnb), "</tr>")
end)
end
defp add_tds(context, row, tag, aligns, lnb) do
Enum.reduce(1..length(row), context, add_td_fn(row, tag, aligns, lnb))
end
defp add_td_fn(row, tag, aligns, lnb) do
fn n, ctx ->
style =
case Enum.at(aligns, n - 1, :default) do
:default -> ""
align -> " style=\"text-align: #{align}\""
end
col = Enum.at(row, n - 1)
converted = convert(col, lnb, set_messages(ctx, []))
append(add_messages_from(ctx, converted), "<#{tag}#{style}>#{converted.value}</#{tag}>")
end
end
###############################
# Append Footnote Return Link #
###############################
defdelegate append_footnote_link(note), to: HtmlRenderer
defdelegate append_footnote_link(note, fnlink), to: HtmlRenderer
defdelegate render_code(lines), to: HtmlRenderer
defp code_classes(language, prefix) do
["" | String.split(prefix || "")]
|> Enum.map(fn pfx -> "#{pfx}#{language}" end)
|> Enum.join(" ")
end
end

View file

@ -13,21 +13,33 @@ def cast(object) when is_binary(object) do
cast([object]) cast([object])
end end
def cast(data) when is_list(data) do def cast(object) when is_map(object) do
data case ObjectID.cast(object) do
|> Enum.reduce_while({:ok, []}, fn element, {:ok, list} -> {:ok, data} -> {:ok, [data]}
case ObjectID.cast(element) do _ -> :error
{:ok, id} -> end
{:cont, {:ok, [id | list]}}
_ ->
{:halt, :error}
end
end)
end end
def cast(_) do def cast(data) when is_list(data) do
:error data =
data
|> Enum.reduce_while([], fn element, list ->
case ObjectID.cast(element) do
{:ok, id} ->
{:cont, [id | list]}
_ ->
{:cont, list}
end
end)
|> Enum.sort()
|> Enum.uniq()
{:ok, data}
end
def cast(data) do
{:error, data}
end end
def dump(data) do def dump(data) do

View file

@ -73,7 +73,7 @@ def report(to, reporter, account, statuses, comment) do
#{comment_html} #{comment_html}
#{statuses_html} #{statuses_html}
<p> <p>
<a href="#{Pleroma.Web.base_url()}/pleroma/admin/#/reports/index">View Reports in AdminFE</a> <a href="#{Pleroma.Web.Endpoint.url()}/pleroma/admin/#/reports/index">View Reports in AdminFE</a>
""" """
new() new()
@ -87,7 +87,7 @@ def new_unapproved_registration(to, account) do
html_body = """ html_body = """
<p>New account for review: <a href="#{account.ap_id}">@#{account.nickname}</a></p> <p>New account for review: <a href="#{account.ap_id}">@#{account.nickname}</a></p>
<blockquote>#{HTML.strip_tags(account.registration_reason)}</blockquote> <blockquote>#{HTML.strip_tags(account.registration_reason)}</blockquote>
<a href="#{Pleroma.Web.base_url()}/pleroma/admin/#/users/#{account.id}/">Visit AdminFE</a> <a href="#{Pleroma.Web.Endpoint.url()}/pleroma/admin/#/users/#{account.id}/">Visit AdminFE</a>
""" """
new() new()

View file

@ -5,15 +5,22 @@
defmodule Pleroma.Emails.UserEmail do defmodule Pleroma.Emails.UserEmail do
@moduledoc "User emails" @moduledoc "User emails"
use Phoenix.Swoosh, view: Pleroma.Web.EmailView, layout: {Pleroma.Web.LayoutView, :email}
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router alias Pleroma.Web.Router
import Swoosh.Email
import Phoenix.Swoosh, except: [render_body: 3]
import Pleroma.Config.Helpers, only: [instance_name: 0, sender: 0] import Pleroma.Config.Helpers, only: [instance_name: 0, sender: 0]
def render_body(email, template, assigns \\ %{}) do
email
|> put_new_layout({Pleroma.Web.LayoutView, :email})
|> put_new_view(Pleroma.Web.EmailView)
|> Phoenix.Swoosh.render_body(template, assigns)
end
defp recipient(email, nil), do: email defp recipient(email, nil), do: email
defp recipient(email, name), do: {name, email} defp recipient(email, name), do: {name, email}
defp recipient(%User{} = user), do: recipient(user.email, user.name) defp recipient(%User{} = user), do: recipient(user.email, user.name)

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Emoji.Formatter do defmodule Pleroma.Emoji.Formatter do
alias Pleroma.Emoji alias Pleroma.Emoji
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.Web alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
def emojify(text) do def emojify(text) do
@ -44,7 +44,7 @@ def get_emoji_map(text) when is_binary(text) do
Emoji.get_all() Emoji.get_all()
|> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end) |> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end)
|> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc -> |> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->
Map.put(acc, name, to_string(URI.merge(Web.base_url(), file))) Map.put(acc, name, to_string(URI.merge(Endpoint.url(), file)))
end) end)
end end

View file

@ -62,7 +62,7 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag) tag = String.downcase(tag)
url = "#{Pleroma.Web.base_url()}/tag/#{tag}" url = "#{Pleroma.Web.Endpoint.url()}/tag/#{tag}"
link = link =
Phoenix.HTML.Tag.content_tag(:a, tag_text, Phoenix.HTML.Tag.content_tag(:a, tag_text,
@ -121,6 +121,10 @@ def mentions_escape(text, options \\ []) do
end end
end end
def markdown_to_html(text) do
Earmark.as_html!(text, %Earmark.Options{compact_output: true})
end
def html_escape({text, mentions, hashtags}, type) do def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags} {html_escape(text, type), mentions, hashtags}
end end

View file

@ -11,9 +11,7 @@ defmodule Pleroma.Gun do
@callback await(pid(), reference()) :: {:response, :fin, 200, []} @callback await(pid(), reference()) :: {:response, :fin, 200, []}
@callback set_owner(pid(), pid()) :: :ok @callback set_owner(pid(), pid()) :: :ok
@api Pleroma.Config.get([Pleroma.Gun], Pleroma.Gun.API) defp api, do: Pleroma.Config.get([Pleroma.Gun], Pleroma.Gun.API)
defp api, do: @api
def open(host, port, opts), do: api().open(host, port, opts) def open(host, port, opts), do: api().open(host, port, opts)

View file

@ -5,11 +5,11 @@
defmodule Pleroma.Gun.ConnectionPool.Reclaimer do defmodule Pleroma.Gun.ConnectionPool.Reclaimer do
use GenServer, restart: :temporary use GenServer, restart: :temporary
@registry Pleroma.Gun.ConnectionPool defp registry, do: Pleroma.Gun.ConnectionPool
def start_monitor do def start_monitor do
pid = pid =
case :gen_server.start(__MODULE__, [], name: {:via, Registry, {@registry, "reclaimer"}}) do case :gen_server.start(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do
{:ok, pid} -> {:ok, pid} ->
pid pid
@ -46,7 +46,7 @@ def handle_continue(:reclaim, _) do
# {worker_pid, crf, last_reference} end) # {worker_pid, crf, last_reference} end)
unused_conns = unused_conns =
Registry.select( Registry.select(
@registry, registry(),
[ [
{{:_, :"$1", {:_, :"$2", :"$3", :"$4"}}, [{:==, :"$2", []}], [{{:"$1", :"$3", :"$4"}}]} {{:_, :"$1", {:_, :"$2", :"$3", :"$4"}}, [{:==, :"$2", []}], [{{:"$1", :"$3", :"$4"}}]}
] ]

View file

@ -6,10 +6,10 @@ defmodule Pleroma.Gun.ConnectionPool.Worker do
alias Pleroma.Gun alias Pleroma.Gun
use GenServer, restart: :temporary use GenServer, restart: :temporary
@registry Pleroma.Gun.ConnectionPool defp registry, do: Pleroma.Gun.ConnectionPool
def start_link([key | _] = opts) do def start_link([key | _] = opts) do
GenServer.start_link(__MODULE__, opts, name: {:via, Registry, {@registry, key}}) GenServer.start_link(__MODULE__, opts, name: {:via, Registry, {registry(), key}})
end end
@impl true @impl true
@ -24,7 +24,7 @@ def handle_continue({:connect, [key, uri, opts, client_pid]}, _) do
time = :erlang.monotonic_time(:millisecond) time = :erlang.monotonic_time(:millisecond)
{_, _} = {_, _} =
Registry.update_value(@registry, key, fn _ -> Registry.update_value(registry(), key, fn _ ->
{conn_pid, [client_pid], 1, time} {conn_pid, [client_pid], 1, time}
end) end)
@ -65,7 +65,7 @@ def handle_call(:add_client, {client_pid, _}, %{key: key, protocol: protocol} =
time = :erlang.monotonic_time(:millisecond) time = :erlang.monotonic_time(:millisecond)
{{conn_pid, used_by, _, _}, _} = {{conn_pid, used_by, _, _}, _} =
Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} -> Registry.update_value(registry(), key, fn {conn_pid, used_by, crf, last_reference} ->
{conn_pid, [client_pid | used_by], crf(time - last_reference, crf), time} {conn_pid, [client_pid | used_by], crf(time - last_reference, crf), time}
end) end)
@ -92,7 +92,7 @@ def handle_call(:add_client, {client_pid, _}, %{key: key, protocol: protocol} =
@impl true @impl true
def handle_call(:remove_client, {client_pid, _}, %{key: key} = state) do def handle_call(:remove_client, {client_pid, _}, %{key: key} = state) do
{{_conn_pid, used_by, _crf, _last_reference}, _} = {{_conn_pid, used_by, _crf, _last_reference}, _} =
Registry.update_value(@registry, key, fn {conn_pid, used_by, crf, last_reference} -> Registry.update_value(registry(), key, fn {conn_pid, used_by, crf, last_reference} ->
{conn_pid, List.delete(used_by, client_pid), crf, last_reference} {conn_pid, List.delete(used_by, client_pid), crf, last_reference}
end) end)

View file

@ -49,31 +49,6 @@ def filter_tags(html, scrubber) do
def filter_tags(html), do: filter_tags(html, nil) def filter_tags(html), do: filter_tags(html, nil)
def strip_tags(html), do: filter_tags(html, FastSanitize.Sanitizer.StripTags) def strip_tags(html), do: filter_tags(html, FastSanitize.Sanitizer.StripTags)
def get_cached_scrubbed_html_for_activity(
content,
scrubbers,
activity,
key \\ "",
callback \\ fn x -> x end
) do
key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
@cachex.fetch!(:scrubber_cache, key, fn _key ->
object = Pleroma.Object.normalize(activity, fetch: false)
ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
end)
end
def get_cached_stripped_html_for_activity(content, activity, key) do
get_cached_scrubbed_html_for_activity(
content,
FastSanitize.Sanitizer.StripTags,
activity,
key,
&HtmlEntities.decode/1
)
end
def ensure_scrubbed_html( def ensure_scrubbed_html(
content, content,
scrubbers, scrubbers,
@ -92,16 +67,6 @@ def ensure_scrubbed_html(
end end
end end
defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do
generate_scrubber_signature([scrubber])
end
defp generate_scrubber_signature(scrubbers) do
Enum.reduce(scrubbers, "", fn scrubber, signature ->
"#{signature}#{to_string(scrubber)}"
end)
end
def extract_first_external_url_from_object(%{data: %{"content" => content}} = object) def extract_first_external_url_from_object(%{data: %{"content" => content}} = object)
when is_binary(content) do when is_binary(content) do
unless object.data["fake"] do unless object.data["fake"] do

View file

@ -54,8 +54,8 @@ def pool_timeout(pool) do
Config.get([:pools, pool, :recv_timeout], default) Config.get([:pools, pool, :recv_timeout], default)
end end
@prefix Pleroma.Gun.ConnectionPool
def limiter_setup do def limiter_setup do
prefix = Pleroma.Gun.ConnectionPool
wait = Config.get([:connections_pool, :connection_acquisition_wait]) wait = Config.get([:connections_pool, :connection_acquisition_wait])
retries = Config.get([:connections_pool, :connection_acquisition_retries]) retries = Config.get([:connections_pool, :connection_acquisition_retries])
@ -66,7 +66,7 @@ def limiter_setup do
max_waiting = Keyword.get(opts, :max_waiting, 10) max_waiting = Keyword.get(opts, :max_waiting, 10)
result = result =
ConcurrentLimiter.new(:"#{@prefix}.#{name}", max_running, max_waiting, ConcurrentLimiter.new(:"#{prefix}.#{name}", max_running, max_waiting,
wait: wait, wait: wait,
max_retries: retries max_retries: retries
) )

View file

@ -5,8 +5,8 @@
defmodule Pleroma.HTTP.WebPush do defmodule Pleroma.HTTP.WebPush do
@moduledoc false @moduledoc false
def post(url, payload, headers) do def post(url, payload, headers, options \\ []) do
list_headers = Map.to_list(headers) list_headers = Map.to_list(headers)
Pleroma.HTTP.post(url, payload, list_headers) Pleroma.HTTP.post(url, payload, list_headers, options)
end end
end end

View file

@ -12,4 +12,10 @@ def put_if_present(map, key, value, value_function \\ &{:ok, &1}) when is_map(ma
_ -> map _ -> map
end end
end end
def safe_put_in(data, keys, value) when is_map(data) and is_list(keys) do
Kernel.put_in(data, keys, value)
rescue
_ -> data
end
end end

View file

@ -366,7 +366,7 @@ def update_data(%Object{data: data} = object, attrs \\ %{}) do
end end
def local?(%Object{data: %{"id" => id}}) do def local?(%Object{data: %{"id" => id}}) do
String.starts_with?(id, Pleroma.Web.base_url() <> "/") String.starts_with?(id, Pleroma.Web.Endpoint.url() <> "/")
end end
def replies(object, opts \\ []) do def replies(object, opts \\ []) do

View file

@ -71,6 +71,14 @@ def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(oth
compare_uris(id_uri, other_uri) compare_uris(id_uri, other_uri)
end end
# Mastodon pin activities don't have an id, so we check the object field, which will be pinned.
def contain_origin_from_id(id, %{"object" => object}) when is_binary(object) do
id_uri = URI.parse(id)
object_uri = URI.parse(object)
compare_uris(id_uri, object_uri)
end
def contain_origin_from_id(_id, _data), do: :error def contain_origin_from_id(_id, _data), do: :error
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}), def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Object.Fetcher do defmodule Pleroma.Object.Fetcher do
alias Pleroma.HTTP alias Pleroma.HTTP
alias Pleroma.Maps
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.Repo alias Pleroma.Repo
@ -101,6 +102,9 @@ def fetch_object_from_id(id, options \\ []) do
{:transmogrifier, {:error, {:reject, e}}} -> {:transmogrifier, {:error, {:reject, e}}} ->
{:reject, e} {:reject, e}
{:transmogrifier, {:reject, e}} ->
{:reject, e}
{:transmogrifier, _} = e -> {:transmogrifier, _} = e ->
{:error, e} {:error, e}
@ -124,12 +128,14 @@ def fetch_object_from_id(id, options \\ []) do
defp prepare_activity_params(data) do defp prepare_activity_params(data) do
%{ %{
"type" => "Create", "type" => "Create",
"to" => data["to"] || [],
"cc" => data["cc"] || [],
# Should we seriously keep this attributedTo thing? # Should we seriously keep this attributedTo thing?
"actor" => data["actor"] || data["attributedTo"], "actor" => data["actor"] || data["attributedTo"],
"object" => data "object" => data
} }
|> Maps.put_if_present("to", data["to"])
|> Maps.put_if_present("cc", data["cc"])
|> Maps.put_if_present("bto", data["bto"])
|> Maps.put_if_present("bcc", data["bcc"])
end end
def fetch_object_from_id!(id, options \\ []) do def fetch_object_from_id!(id, options \\ []) do

View file

@ -23,6 +23,9 @@ defmodule Pleroma.Upload do
is once created permanent and changing it (especially in uploaders) is probably a bad idea! is once created permanent and changing it (especially in uploaders) is probably a bad idea!
* `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the * `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the
path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over. path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over.
* `:width` - width of the media in pixels
* `:height` - height of the media in pixels
* `:blurhash` - string hash of the image encoded with the blurhash algorithm (https://blurha.sh/)
Related behaviors: Related behaviors:
@ -32,6 +35,7 @@ defmodule Pleroma.Upload do
""" """
alias Ecto.UUID alias Ecto.UUID
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Maps
require Logger require Logger
@type source :: @type source ::
@ -53,9 +57,12 @@ defmodule Pleroma.Upload do
name: String.t(), name: String.t(),
tempfile: String.t(), tempfile: String.t(),
content_type: String.t(), content_type: String.t(),
width: integer(),
height: integer(),
blurhash: String.t(),
path: String.t() path: String.t()
} }
defstruct [:id, :name, :tempfile, :content_type, :path] defstruct [:id, :name, :tempfile, :content_type, :width, :height, :blurhash, :path]
defp get_description(opts, upload) do defp get_description(opts, upload) do
case {opts[:description], Pleroma.Config.get([Pleroma.Upload, :default_description])} do case {opts[:description], Pleroma.Config.get([Pleroma.Upload, :default_description])} do
@ -89,9 +96,12 @@ def store(upload, opts \\ []) do
"mediaType" => upload.content_type, "mediaType" => upload.content_type,
"href" => url_from_spec(upload, opts.base_url, url_spec) "href" => url_from_spec(upload, opts.base_url, url_spec)
} }
|> Maps.put_if_present("width", upload.width)
|> Maps.put_if_present("height", upload.height)
], ],
"name" => description "name" => description
}} }
|> Maps.put_if_present("blurhash", upload.blurhash)}
else else
{:description_limit, _} -> {:description_limit, _} ->
{:error, :description_too_long} {:error, :description_too_long}
@ -225,7 +235,7 @@ def base_url do
case uploader do case uploader do
Pleroma.Uploaders.Local -> Pleroma.Uploaders.Local ->
upload_base_url || Pleroma.Web.base_url() <> "/media/" upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/"
Pleroma.Uploaders.S3 -> Pleroma.Uploaders.S3 ->
bucket = Config.get([Pleroma.Uploaders.S3, :bucket]) bucket = Config.get([Pleroma.Uploaders.S3, :bucket])
@ -251,7 +261,7 @@ def base_url do
end end
_ -> _ ->
public_endpoint || upload_base_url || Pleroma.Web.base_url() <> "/media/" public_endpoint || upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/"
end end
end end
end end

View file

@ -0,0 +1,45 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.AnalyzeMetadata do
@moduledoc """
Extracts metadata about the upload, such as width/height
"""
require Logger
@behaviour Pleroma.Upload.Filter
@spec filter(Pleroma.Upload.t()) ::
{:ok, :filtered, Pleroma.Upload.t()} | {:ok, :noop} | {:error, String.t()}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _} = upload) do
try do
image =
file
|> Mogrify.open()
|> Mogrify.verbose()
upload =
upload
|> Map.put(:width, image.width)
|> Map.put(:height, image.height)
|> Map.put(:blurhash, get_blurhash(file))
{:ok, :filtered, upload}
rescue
e in ErlangError ->
Logger.warn("#{__MODULE__}: #{inspect(e)}")
{:ok, :noop}
end
end
def filter(_), do: {:ok, :noop}
defp get_blurhash(file) do
with {:ok, blurhash} <- :eblurhash.magick(file) do
blurhash
else
_ -> nil
end
end
end

View file

@ -35,7 +35,7 @@ defmodule Pleroma.Uploaders.Uploader do
""" """
@type file_spec :: {:file | :url, String.t()} @type file_spec :: {:file | :url, String.t()}
@callback put_file(Pleroma.Upload.t()) :: @callback put_file(upload :: struct()) ::
:ok | {:ok, file_spec()} | {:error, String.t()} | :wait_callback :ok | {:ok, file_spec()} | {:error, String.t()} | :wait_callback
@callback delete_file(file :: String.t()) :: :ok | {:error, String.t()} @callback delete_file(file :: String.t()) :: :ok | {:error, String.t()}
@ -46,7 +46,7 @@ defmodule Pleroma.Uploaders.Uploader do
| {:error, Plug.Conn.t(), String.t()} | {:error, Plug.Conn.t(), String.t()}
@optional_callbacks http_callback: 2 @optional_callbacks http_callback: 2
@spec put_file(module(), Pleroma.Upload.t()) :: {:ok, file_spec()} | {:error, String.t()} @spec put_file(module(), upload :: struct()) :: {:ok, file_spec()} | {:error, String.t()}
def put_file(uploader, upload) do def put_file(uploader, upload) do
case uploader.put_file(upload) do case uploader.put_file(upload) do
:ok -> {:ok, {:file, upload.path}} :ok -> {:ok, {:file, upload.path}}

View file

@ -27,13 +27,13 @@ defmodule Pleroma.User do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship alias Pleroma.UserRelationship
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.Endpoint
alias Pleroma.Web.OAuth alias Pleroma.Web.OAuth
alias Pleroma.Web.RelMe alias Pleroma.Web.RelMe
alias Pleroma.Workers.BackgroundWorker alias Pleroma.Workers.BackgroundWorker
@ -99,6 +99,7 @@ defmodule Pleroma.User do
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)
field(:featured_address, :string)
field(:search_rank, :float, virtual: true) field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true) field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: []) field(:tags, {:array, :string}, default: [])
@ -129,7 +130,6 @@ defmodule Pleroma.User do
field(:hide_followers, :boolean, default: false) field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false) field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true) field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false}) field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil) field(:mascot, :map, default: nil)
field(:emoji, :map, default: %{}) field(:emoji, :map, default: %{})
@ -147,6 +147,7 @@ defmodule Pleroma.User do
field(:accepts_chat_messages, :boolean, default: nil) field(:accepts_chat_messages, :boolean, default: nil)
field(:last_active_at, :naive_datetime) field(:last_active_at, :naive_datetime)
field(:disclose_client, :boolean, default: true) field(:disclose_client, :boolean, default: true)
field(:pinned_objects, :map, default: %{})
embeds_one( embeds_one(
:notification_settings, :notification_settings,
@ -358,7 +359,7 @@ def avatar_url(user, options \\ []) do
_ -> _ ->
unless options[:no_default] do unless options[:no_default] do
Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png") Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
end end
end end
end end
@ -366,13 +367,15 @@ def avatar_url(user, options \\ []) do
def banner_url(user, options \\ []) do def banner_url(user, options \\ []) do
case user.banner do case user.banner do
%{"url" => [%{"href" => href} | _]} -> href %{"url" => [%{"href" => href} | _]} -> href
_ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png" _ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png"
end end
end end
# Should probably be renamed or removed # Should probably be renamed or removed
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}" @spec ap_id(User.t()) :: String.t()
def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
@spec ap_followers(User.t()) :: String.t()
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
@ -380,6 +383,11 @@ def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following" def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
@spec ap_featured_collection(User.t()) :: String.t()
def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa
def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured"
defp truncate_fields_param(params) do defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do if Map.has_key?(params, :fields) do
Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1)) Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
@ -442,6 +450,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
:uri, :uri,
:follower_address, :follower_address,
:following_address, :following_address,
:featured_address,
:hide_followers, :hide_followers,
:hide_follows, :hide_follows,
:hide_followers_count, :hide_followers_count,
@ -453,7 +462,8 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
:invisible, :invisible,
:actor_type, :actor_type,
:also_known_as, :also_known_as,
:accepts_chat_messages :accepts_chat_messages,
:pinned_objects
] ]
) )
|> cast(params, [:name], empty_values: []) |> cast(params, [:name], empty_values: [])
@ -685,7 +695,7 @@ def register_changeset_ldap(struct, params = %{password: password})
|> validate_format(:nickname, local_nickname_regex()) |> validate_format(:nickname, local_nickname_regex())
|> put_ap_id() |> put_ap_id()
|> unique_constraint(:ap_id) |> unique_constraint(:ap_id)
|> put_following_and_follower_address() |> put_following_and_follower_and_featured_address()
end end
def register_changeset(struct, params \\ %{}, opts \\ []) do def register_changeset(struct, params \\ %{}, opts \\ []) do
@ -746,7 +756,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|> put_password_hash |> put_password_hash
|> put_ap_id() |> put_ap_id()
|> unique_constraint(:ap_id) |> unique_constraint(:ap_id)
|> put_following_and_follower_address() |> put_following_and_follower_and_featured_address()
end end
def maybe_validate_required_email(changeset, true), do: changeset def maybe_validate_required_email(changeset, true), do: changeset
@ -764,11 +774,16 @@ defp put_ap_id(changeset) do
put_change(changeset, :ap_id, ap_id) put_change(changeset, :ap_id, ap_id)
end end
defp put_following_and_follower_address(changeset) do defp put_following_and_follower_and_featured_address(changeset) do
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)}) user = %User{nickname: get_field(changeset, :nickname)}
followers = ap_followers(user)
following = ap_following(user)
featured = ap_featured_collection(user)
changeset changeset
|> put_change(:follower_address, followers) |> put_change(:follower_address, followers)
|> put_change(:following_address, following)
|> put_change(:featured_address, featured)
end end
defp autofollow_users(user) do defp autofollow_users(user) do
@ -2334,45 +2349,35 @@ def approval_changeset(user, set_approval: approved?) do
cast(user, %{is_approved: approved?}, [:is_approved]) cast(user, %{is_approved: approved?}, [:is_approved])
end end
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
if id not in user.pinned_activities do def add_pinned_object_id(%User{} = user, object_id) do
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0) if !user.pinned_objects[object_id] do
params = %{pinned_activities: user.pinned_activities ++ [id]} params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}
# if pinned activity was scheduled for deletion, we remove job
if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(id) do
Oban.cancel_job(expiration.id)
end
user user
|> cast(params, [:pinned_activities]) |> cast(params, [:pinned_objects])
|> validate_length(:pinned_activities, |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
max: max_pinned_statuses, max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
message: "You have already pinned the maximum number of statuses"
) if Enum.count(pinned_objects) <= max_pinned_statuses do
[]
else
[pinned_objects: "You have already pinned the maximum number of statuses"]
end
end)
else else
change(user) change(user)
end end
|> update_and_set_cache() |> update_and_set_cache()
end end
def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
params = %{pinned_activities: List.delete(user.pinned_activities, id)} def remove_pinned_object_id(%User{} = user, object_id) do
# if pinned activity was scheduled for deletion, we reschedule it for deletion
if data["expires_at"] do
# MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation
{:ok, expires_at} =
data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast()
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
activity_id: id,
expires_at: expires_at
})
end
user user
|> cast(params, [:pinned_activities]) |> cast(
%{pinned_objects: Map.delete(user.pinned_objects, object_id)},
[:pinned_objects]
)
|> update_and_set_cache() |> update_and_set_cache()
end end

View file

@ -11,6 +11,8 @@ defmodule Pleroma.Utils do
eperm epipe erange erofs espipe esrch estale etxtbsy exdev eperm epipe erange erofs espipe esrch estale etxtbsy exdev
)a )a
@repo_timeout Pleroma.Config.get([Pleroma.Repo, :timeout], 15_000)
def compile_dir(dir) when is_binary(dir) do def compile_dir(dir) when is_binary(dir) do
dir dir
|> File.ls!() |> File.ls!()
@ -63,4 +65,21 @@ def posix_error_message(code) when code in @posix_error_codes do
end end
def posix_error_message(_), do: "" def posix_error_message(_), do: ""
@doc """
Returns [timeout: integer] suitable for passing as an option to Repo functions.
This function detects if the execution was triggered from IEx shell, Mix task, or
./bin/pleroma_ctl and sets the timeout to :infinity, else returns the default timeout value.
"""
@spec query_timeout() :: [timeout: integer]
def query_timeout do
{parent, _, _, _} = Process.info(self(), :current_stacktrace) |> elem(1) |> Enum.fetch!(2)
cond do
parent |> to_string |> String.starts_with?("Elixir.Mix.Task") -> [timeout: :infinity]
parent == :erl_eval -> [timeout: :infinity]
true -> [timeout: @repo_timeout]
end
end
end end

View file

@ -35,9 +35,10 @@ def controller do
import Plug.Conn import Plug.Conn
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
import Pleroma.Web.Router.Helpers
import Pleroma.Web.TranslationHelpers import Pleroma.Web.TranslationHelpers
alias Pleroma.Web.Router.Helpers, as: Routes
plug(:set_put_layout) plug(:set_put_layout)
defp set_put_layout(conn, _) do defp set_put_layout(conn, _) do
@ -131,7 +132,8 @@ def view do
import Pleroma.Web.ErrorHelpers import Pleroma.Web.ErrorHelpers
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
import Pleroma.Web.Router.Helpers
alias Pleroma.Web.Router.Helpers, as: Routes
require Logger require Logger
@ -229,20 +231,4 @@ def call(%Plug.Conn{} = conn, options) do
defmacro __using__(which) when is_atom(which) do defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, []) apply(__MODULE__, which, [])
end end
def base_url do
Pleroma.Web.Endpoint.url()
end
# TODO: Change to Phoenix.Router.routes/1 for Phoenix 1.6.0+
def get_api_routes do
Pleroma.Web.Router.__routes__()
|> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end)
|> Enum.map(fn r ->
r.path
|> String.split("/", trim: true)
|> List.first()
end)
|> Enum.uniq()
end
end end

View file

@ -88,7 +88,7 @@ defp increase_replies_count_if_reply(%{
defp increase_replies_count_if_reply(_create_data), do: :noop defp increase_replies_count_if_reply(_create_data), do: :noop
@object_types ~w[ChatMessage Question Answer Audio Video Event Article] @object_types ~w[ChatMessage Question Answer Audio Video Event Article Note]
@impl true @impl true
def persist(%{"type" => type} = object, meta) when type in @object_types do def persist(%{"type" => type} = object, meta) when type in @object_types do
with {:ok, object} <- Object.create(object) do with {:ok, object} <- Object.create(object) do
@ -630,7 +630,7 @@ defp fetch_activities_for_user(user, reading_user, params) do
|> Map.put(:type, ["Create", "Announce"]) |> Map.put(:type, ["Create", "Announce"])
|> Map.put(:user, reading_user) |> Map.put(:user, reading_user)
|> Map.put(:actor_id, user.ap_id) |> Map.put(:actor_id, user.ap_id)
|> Map.put(:pinned_activity_ids, user.pinned_activities) |> Map.put(:pinned_object_ids, Map.keys(user.pinned_objects))
params = params =
if User.blocks?(reading_user, user) do if User.blocks?(reading_user, user) do
@ -1075,8 +1075,18 @@ defp restrict_unlisted(query, %{restrict_unlisted: true}) do
defp restrict_unlisted(query, _), do: query defp restrict_unlisted(query, _), do: query
defp restrict_pinned(query, %{pinned: true, pinned_activity_ids: ids}) do defp restrict_pinned(query, %{pinned: true, pinned_object_ids: ids}) do
from(activity in query, where: activity.id in ^ids) from(
[activity, object: o] in query,
where:
fragment(
"(?)->>'type' = 'Create' and coalesce((?)->'object'->>'id', (?)->>'object') = any (?)",
activity.data,
activity.data,
activity.data,
^ids
)
)
end end
defp restrict_pinned(query, _), do: query defp restrict_pinned(query, _), do: query
@ -1419,6 +1429,9 @@ defp object_to_user_data(data) do
invisible = data["invisible"] || false invisible = data["invisible"] || false
actor_type = data["type"] || "Person" actor_type = data["type"] || "Person"
featured_address = data["featured"]
{:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address)
public_key = public_key =
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
data["publicKey"]["publicKeyPem"] data["publicKey"]["publicKeyPem"]
@ -1447,13 +1460,15 @@ defp object_to_user_data(data) do
name: data["name"], name: data["name"],
follower_address: data["followers"], follower_address: data["followers"],
following_address: data["following"], following_address: data["following"],
featured_address: featured_address,
bio: data["summary"] || "", bio: data["summary"] || "",
actor_type: actor_type, actor_type: actor_type,
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 accepts_chat_messages: accepts_chat_messages,
pinned_objects: pinned_objects
} }
# nickname can be nil because of virtual actors # nickname can be nil because of virtual actors
@ -1591,6 +1606,41 @@ def maybe_handle_clashing_nickname(data) do
end end
end end
def pin_data_from_featured_collection(%{
"type" => type,
"orderedItems" => objects
})
when type in ["OrderedCollection", "Collection"] do
Map.new(objects, fn %{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()} end)
end
def fetch_and_prepare_featured_from_ap_id(nil) do
{:ok, %{}}
end
def fetch_and_prepare_featured_from_ap_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do
{:ok, pin_data_from_featured_collection(data)}
else
e ->
Logger.error("Could not decode featured collection at fetch #{ap_id}, #{inspect(e)}")
{:ok, %{}}
end
end
def pinned_fetch_task(nil), do: nil
def pinned_fetch_task(%{pinned_objects: pins}) do
if Enum.all?(pins, fn {ap_id, _} ->
Object.get_cached_by_ap_id(ap_id) ||
match?({:ok, _object}, Fetcher.fetch_object_from_id(ap_id))
end) do
:ok
else
:error
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)
@ -1598,6 +1648,8 @@ def make_user_from_ap_id(ap_id) do
Transmogrifier.upgrade_user_from_ap_id(ap_id) Transmogrifier.upgrade_user_from_ap_id(ap_id)
else else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
if user do if user do
user user
|> User.remote_user_changeset(data) |> User.remote_user_changeset(data)

View file

@ -3,5 +3,5 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub.Persisting do defmodule Pleroma.Web.ActivityPub.ActivityPub.Persisting do
@callback persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} @callback persist(map(), keyword()) :: {:ok, struct()}
end end

View file

@ -3,10 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub.Streaming do defmodule Pleroma.Web.ActivityPub.ActivityPub.Streaming do
alias Pleroma.Activity @callback stream_out(struct()) :: any()
alias Pleroma.Object @callback stream_out_participations(struct(), struct()) :: any()
alias Pleroma.User
@callback stream_out(Activity.t()) :: any()
@callback stream_out_participations(Object.t(), User.t()) :: any()
end end

View file

@ -543,4 +543,12 @@ def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} =
|> json(object.data) |> json(object.data)
end end
end end
def pinned(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("featured.json", %{user: user}))
end
end
end end

View file

@ -223,7 +223,7 @@ def announce(actor, object, options \\ []) do
[actor.follower_address] [actor.follower_address]
public? and Visibility.is_local_public?(object) -> public? and Visibility.is_local_public?(object) ->
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_local_public()] [actor.follower_address, object.data["actor"], Utils.as_local_public()]
public? -> public? ->
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()] [actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()]
@ -273,4 +273,36 @@ defp object_action(actor, object) do
"context" => object.data["context"] "context" => object.data["context"]
}, []} }, []}
end end
@spec pin(User.t(), Object.t()) :: {:ok, map(), keyword()}
def pin(%User{} = user, object) do
{:ok,
%{
"id" => Utils.generate_activity_id(),
"target" => pinned_url(user.nickname),
"object" => object.data["id"],
"actor" => user.ap_id,
"type" => "Add",
"to" => [Pleroma.Constants.as_public()],
"cc" => [user.follower_address]
}, []}
end
@spec unpin(User.t(), Object.t()) :: {:ok, map, keyword()}
def unpin(%User{} = user, object) do
{:ok,
%{
"id" => Utils.generate_activity_id(),
"target" => pinned_url(user.nickname),
"object" => object.data["id"],
"actor" => user.ap_id,
"type" => "Remove",
"to" => [Pleroma.Constants.as_public()],
"cc" => [user.follower_address]
}, []}
end
defp pinned_url(nickname) when is_binary(nickname) do
Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
end
end end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
@moduledoc "Filter local activities which have no content" @moduledoc "Filter local activities which have no content"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web alias Pleroma.Web.Endpoint
@impl true @impl true
def filter(%{"actor" => actor} = object) do def filter(%{"actor" => actor} = object) do
@ -24,7 +24,7 @@ def filter(%{"actor" => actor} = object) do
def filter(object), do: {:ok, object} def filter(object), do: {:ok, object}
defp is_local?(actor) do defp is_local?(actor) do
if actor |> String.starts_with?("#{Web.base_url()}") do if actor |> String.starts_with?("#{Endpoint.url()}") do
true true
else else
false false

View file

@ -177,6 +177,14 @@ defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image
defp check_banner_removal(_actor_info, object), do: {:ok, object} defp check_banner_removal(_actor_info, object), do: {:ok, object}
defp check_object(%{"object" => object} = activity) do
with {:ok, _object} <- filter(object) do
{:ok, activity}
end
end
defp check_object(object), do: {:ok, object}
@impl true @impl true
def filter(%{"type" => "Delete", "actor" => actor} = object) do def filter(%{"type" => "Delete", "actor" => actor} = object) do
%{host: actor_host} = URI.parse(actor) %{host: actor_host} = URI.parse(actor)
@ -202,7 +210,8 @@ def filter(%{"actor" => actor} = object) do
{:ok, object} <- check_media_nsfw(actor_info, object), {:ok, object} <- check_media_nsfw(actor_info, object),
{:ok, object} <- check_ftl_removal(actor_info, object), {:ok, object} <- check_ftl_removal(actor_info, object),
{:ok, object} <- check_followers_only(actor_info, object), {:ok, object} <- check_followers_only(actor_info, object),
{:ok, object} <- check_report_removal(actor_info, object) do {:ok, object} <- check_report_removal(actor_info, object),
{:ok, object} <- check_object(object) do
{:ok, object} {:ok, object}
else else
{:reject, nil} -> {:reject, "[SimplePolicy]"} {:reject, nil} -> {:reject, "[SimplePolicy]"}
@ -227,6 +236,19 @@ def filter(%{"id" => actor, "type" => obj_type} = object)
end end
end end
def filter(object) when is_binary(object) do
uri = URI.parse(object)
with {:ok, object} <- check_accept(uri, object),
{:ok, object} <- check_reject(uri, object) do
{:ok, object}
else
{:reject, nil} -> {:reject, "[SimplePolicy]"}
{:reject, _} = e -> e
_ -> {:reject, "[SimplePolicy]"}
end
end
def filter(object), do: {:ok, object} def filter(object), do: {:ok, object}
@impl true @impl true

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator
@ -101,7 +102,7 @@ def validate(
%{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity, %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,
meta meta
) )
when objtype in ~w[Question Answer Audio Video Event Article] do when objtype in ~w[Question Answer Audio Video Event Article Note] do
with {:ok, object_data} <- cast_and_apply(object), with {:ok, object_data} <- cast_and_apply(object),
meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
{:ok, create_activity} <- {:ok, create_activity} <-
@ -113,9 +114,35 @@ def validate(
end end
end end
def validate(%{"type" => type} = object, meta)
when type in ~w[Event Question Audio Video Article Note] do
validator =
case type do
"Event" -> EventValidator
"Question" -> QuestionValidator
"Audio" -> AudioVideoValidator
"Video" -> AudioVideoValidator
"Article" -> ArticleNoteValidator
"Note" -> ArticleNoteValidator
end
with {:ok, object} <-
object
|> validator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
# Insert copy of hashtags as strings for the non-hashtag table indexing
tag = (object["tag"] || []) ++ Object.hashtags(%Object{data: object})
object = Map.put(object, "tag", tag)
{:ok, object, meta}
end
end
def validate(%{"type" => type} = object, meta) def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
Event ChatMessage Question Audio Video Article Answer] do ChatMessage Answer] do
validator = validator =
case type do case type do
"Accept" -> AcceptRejectValidator "Accept" -> AcceptRejectValidator
@ -125,12 +152,7 @@ def validate(%{"type" => type} = object, meta)
"Like" -> LikeValidator "Like" -> LikeValidator
"EmojiReact" -> EmojiReactValidator "EmojiReact" -> EmojiReactValidator
"Announce" -> AnnounceValidator "Announce" -> AnnounceValidator
"Event" -> EventValidator
"ChatMessage" -> ChatMessageValidator "ChatMessage" -> ChatMessageValidator
"Question" -> QuestionValidator
"Audio" -> AudioVideoValidator
"Video" -> AudioVideoValidator
"Article" -> ArticleNoteValidator
"Answer" -> AnswerValidator "Answer" -> AnswerValidator
end end
@ -143,6 +165,16 @@ def validate(%{"type" => type} = object, meta)
end end
end end
def validate(%{"type" => type} = object, meta) when type in ~w(Add Remove) do
with {:ok, object} <-
object
|> AddRemoveValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end
def cast_and_apply(%{"type" => "ChatMessage"} = object) do def cast_and_apply(%{"type" => "ChatMessage"} = object) do
ChatMessageValidator.cast_and_apply(object) ChatMessageValidator.cast_and_apply(object)
end end
@ -163,13 +195,13 @@ def cast_and_apply(%{"type" => "Event"} = object) do
EventValidator.cast_and_apply(object) EventValidator.cast_and_apply(object)
end end
def cast_and_apply(%{"type" => "Article"} = object) do def cast_and_apply(%{"type" => type} = object) when type in ~w[Article Note] do
ArticleNoteValidator.cast_and_apply(object) ArticleNoteValidator.cast_and_apply(object)
end end
def cast_and_apply(o), do: {:error, {:validator_not_set, o}} def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
# is_struct/1 isn't present in Elixir 1.8.x # is_struct/1 appears in Elixir 1.11
def stringify_keys(%{__struct__: _} = object) do def stringify_keys(%{__struct__: _} = object) do
object object
|> Map.from_struct() |> Map.from_struct()

View file

@ -27,7 +27,7 @@ def cast_data(data) do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(cng) do defp validate_data(cng) do
cng cng
|> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Accept", "Reject"]) |> validate_inclusion(:type, ["Accept", "Reject"])

View file

@ -0,0 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator do
use Ecto.Schema
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
require Pleroma.Constants
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.User
@primary_key false
embedded_schema do
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:target)
field(:object, ObjectValidators.ObjectID)
field(:actor, ObjectValidators.ObjectID)
field(:type)
field(:to, ObjectValidators.Recipients, default: [])
field(:cc, ObjectValidators.Recipients, default: [])
end
def cast_and_validate(data) do
{:ok, actor} = User.get_or_fetch_by_ap_id(data["actor"])
{:ok, actor} = maybe_refetch_user(actor)
data
|> maybe_fix_data_for_mastodon(actor)
|> cast_data()
|> validate_data(actor)
end
defp maybe_fix_data_for_mastodon(data, actor) do
# Mastodon sends pin/unpin objects without id, to, cc fields
data
|> Map.put_new("id", Pleroma.Web.ActivityPub.Utils.generate_activity_id())
|> Map.put_new("to", [Pleroma.Constants.as_public()])
|> Map.put_new("cc", [actor.follower_address])
end
defp cast_data(data) do
cast(%__MODULE__{}, data, __schema__(:fields))
end
defp validate_data(changeset, actor) do
changeset
|> validate_required([:id, :target, :object, :actor, :type, :to, :cc])
|> validate_inclusion(:type, ~w(Add Remove))
|> validate_actor_presence()
|> validate_collection_belongs_to_actor(actor)
|> validate_object_presence()
end
defp validate_collection_belongs_to_actor(changeset, actor) do
validate_change(changeset, :target, fn :target, target ->
if target == actor.featured_address do
[]
else
[target: "collection doesn't belong to actor"]
end
end)
end
defp maybe_refetch_user(%User{featured_address: address} = user) when is_binary(address) do
{:ok, user}
end
defp maybe_refetch_user(%User{ap_id: ap_id}) do
Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(ap_id)
end
end

View file

@ -50,7 +50,7 @@ def fix_after_cast(cng) do
cng cng
end end
def validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Announce"]) |> validate_inclusion(:type, ["Announce"])
|> validate_required([:id, :type, :object, :actor, :to, :cc]) |> validate_required([:id, :type, :object, :actor, :to, :cc])
@ -68,7 +68,7 @@ def validate_announcable(cng) do
false <- Visibility.is_public?(object) do false <- Visibility.is_public?(object) do
same_actor = object.data["actor"] == actor.ap_id same_actor = object.data["actor"] == actor.ap_id
recipients = get_field(cng, :to) ++ get_field(cng, :cc) recipients = get_field(cng, :to) ++ get_field(cng, :cc)
local_public = Pleroma.Constants.as_local_public() local_public = Utils.as_local_public()
is_public = is_public =
Enum.member?(recipients, Pleroma.Constants.as_public()) or Enum.member?(recipients, Pleroma.Constants.as_public()) or

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do
use Ecto.Schema use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
import Ecto.Changeset import Ecto.Changeset
@ -23,6 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do
field(:name, :string) field(:name, :string)
field(:inReplyTo, ObjectValidators.ObjectID) field(:inReplyTo, ObjectValidators.ObjectID)
field(:attributedTo, ObjectValidators.ObjectID) field(:attributedTo, ObjectValidators.ObjectID)
field(:context, :string)
# TODO: Remove actor on objects # TODO: Remove actor on objects
field(:actor, ObjectValidators.ObjectID) field(:actor, ObjectValidators.ObjectID)
@ -46,11 +48,16 @@ def cast_data(data) do
end end
def changeset(struct, data) do def changeset(struct, data) do
data =
data
|> CommonFixes.fix_actor()
|> CommonFixes.fix_object_defaults()
struct struct
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Answer"]) |> validate_inclusion(:type, ["Answer"])
|> validate_required([:id, :inReplyTo, :name, :attributedTo, :actor]) |> validate_required([:id, :inReplyTo, :name, :attributedTo, :actor])

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
import Ecto.Changeset import Ecto.Changeset
@ -22,8 +23,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do
field(:cc, ObjectValidators.Recipients, default: []) field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: []) field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: []) field(:bcc, ObjectValidators.Recipients, default: [])
# TODO: Write type embeds_many(:tag, TagValidator)
field(:tag, {:array, :map}, default: [])
field(:type, :string) field(:type, :string)
field(:name, :string) field(:name, :string)
@ -50,6 +50,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do
field(:likes, {:array, ObjectValidators.ObjectID}, default: []) field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) field(:announcements, {:array, ObjectValidators.ObjectID}, default: [])
field(:replies, {:array, ObjectValidators.ObjectID}, default: [])
end end
def cast_and_apply(data) do def cast_and_apply(data) do
@ -65,36 +67,51 @@ def cast_and_validate(data) do
end end
def cast_data(data) do def cast_data(data) do
data = fix(data)
%__MODULE__{} %__MODULE__{}
|> changeset(data) |> changeset(data)
end end
defp fix_url(%{"url" => url} = data) when is_map(url) do defp fix_url(%{"url" => url} = data) when is_bitstring(url), do: data
Map.put(data, "url", url["href"]) defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"])
end
defp fix_url(data), do: data defp fix_url(data), do: data
defp fix_tag(%{"tag" => tag} = data) when is_list(tag), do: data
defp fix_tag(%{"tag" => tag} = data) when is_map(tag), do: Map.put(data, "tag", [tag])
defp fix_tag(data), do: Map.drop(data, ["tag"])
defp fix_replies(%{"replies" => %{"first" => %{"items" => replies}}} = data)
when is_list(replies),
do: Map.put(data, "replies", replies)
defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies),
do: Map.put(data, "replies", replies)
defp fix_replies(%{"replies" => replies} = data) when is_bitstring(replies),
do: Map.drop(data, ["replies"])
defp fix_replies(data), do: data
defp fix(data) do defp fix(data) do
data data
|> CommonFixes.fix_defaults()
|> CommonFixes.fix_attribution()
|> CommonFixes.fix_actor() |> CommonFixes.fix_actor()
|> CommonFixes.fix_object_defaults()
|> fix_url() |> fix_url()
|> fix_tag()
|> fix_replies()
|> Transmogrifier.fix_emoji() |> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
end end
def changeset(struct, data) do def changeset(struct, data) do
data = fix(data) data = fix(data)
struct struct
|> cast(data, __schema__(:fields) -- [:attachment]) |> cast(data, __schema__(:fields) -- [:attachment, :tag])
|> cast_embed(:attachment) |> cast_embed(:attachment)
|> cast_embed(:tag)
end end
def validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Article", "Note"]) |> validate_inclusion(:type, ["Article", "Note"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])

View file

@ -6,7 +6,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
use Ecto.Schema use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator
import Ecto.Changeset import Ecto.Changeset
@ -21,6 +20,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
field(:type, :string) field(:type, :string)
field(:href, ObjectValidators.Uri) field(:href, ObjectValidators.Uri)
field(:mediaType, :string, default: "application/octet-stream") field(:mediaType, :string, default: "application/octet-stream")
field(:width, :integer)
field(:height, :integer)
end end
end end
@ -52,7 +53,7 @@ def url_changeset(struct, data) do
data = fix_media_type(data) data = fix_media_type(data)
struct struct
|> cast(data, [:type, :href, :mediaType]) |> cast(data, [:type, :href, :mediaType, :width, :height])
|> validate_inclusion(:type, ["Link"]) |> validate_inclusion(:type, ["Link"])
|> validate_required([:type, :href, :mediaType]) |> validate_required([:type, :href, :mediaType])
end end
@ -60,7 +61,7 @@ def url_changeset(struct, data) do
def fix_media_type(data) do def fix_media_type(data) do
data = Map.put_new(data, "mediaType", data["mimeType"]) data = Map.put_new(data, "mediaType", data["mimeType"])
if MIME.valid?(data["mediaType"]) do if is_bitstring(data["mediaType"]) && MIME.extensions(data["mediaType"]) != [] do
data data
else else
Map.put(data, "mediaType", "application/octet-stream") Map.put(data, "mediaType", "application/octet-stream")
@ -90,7 +91,7 @@ defp fix_url(data) do
end end
end end
def validate_data(cng) do defp validate_data(cng) do
cng cng
|> validate_inclusion(:type, ~w[Document Audio Image Video]) |> validate_inclusion(:type, ~w[Document Audio Image Video])
|> validate_required([:mediaType, :url, :type]) |> validate_required([:mediaType, :url, :type])

View file

@ -5,11 +5,11 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
use Ecto.Schema use Ecto.Schema
alias Pleroma.EarmarkRenderer
alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
import Ecto.Changeset import Ecto.Changeset
@ -23,8 +23,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
field(:cc, ObjectValidators.Recipients, default: []) field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: []) field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: []) field(:bcc, ObjectValidators.Recipients, default: [])
# TODO: Write type embeds_many(:tag, TagValidator)
field(:tag, {:array, :map}, default: [])
field(:type, :string) field(:type, :string)
field(:name, :string) field(:name, :string)
@ -110,7 +109,7 @@ defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = data)
when is_binary(content) do when is_binary(content) do
content = content =
content content
|> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer}) |> Pleroma.Formatter.markdown_to_html()
|> Pleroma.HTML.filter_tags() |> Pleroma.HTML.filter_tags()
Map.put(data, "content", content) Map.put(data, "content", content)
@ -120,9 +119,8 @@ defp fix_content(data), do: data
defp fix(data) do defp fix(data) do
data data
|> CommonFixes.fix_defaults()
|> CommonFixes.fix_attribution()
|> CommonFixes.fix_actor() |> CommonFixes.fix_actor()
|> CommonFixes.fix_object_defaults()
|> Transmogrifier.fix_emoji() |> Transmogrifier.fix_emoji()
|> fix_url() |> fix_url()
|> fix_content() |> fix_content()
@ -132,11 +130,12 @@ def changeset(struct, data) do
data = fix(data) data = fix(data)
struct struct
|> cast(data, __schema__(:fields) -- [:attachment]) |> cast(data, __schema__(:fields) -- [:attachment, :tag])
|> cast_embed(:attachment) |> cast_embed(:attachment)
|> cast_embed(:tag)
end end
def validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Audio", "Video"]) |> validate_inclusion(:type, ["Audio", "Video"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment])

View file

@ -26,7 +26,7 @@ def cast_data(data) do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(cng) do defp validate_data(cng) do
cng cng
|> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Block"]) |> validate_inclusion(:type, ["Block"])

View file

@ -67,7 +67,7 @@ def changeset(struct, data) do
|> cast_embed(:attachment) |> cast_embed(:attachment)
end end
def validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["ChatMessage"]) |> validate_inclusion(:type, ["ChatMessage"])
|> validate_required([:id, :actor, :to, :type, :published]) |> validate_required([:id, :actor, :to, :type, :published])

View file

@ -3,26 +3,55 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
# based on Pleroma.Web.ActivityPub.Utils.lazy_put_objects_defaults def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
def fix_defaults(data) do {:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback)
data =
Enum.reject(data, fn x ->
String.ends_with?(x, "/followers") and x != follower_collection
end)
Map.put(message, field, data)
end
def fix_object_defaults(data) do
%{data: %{"id" => context}, id: context_id} = %{data: %{"id" => context}, id: context_id} =
Utils.create_context(data["context"] || data["conversation"]) Utils.create_context(data["context"] || data["conversation"])
%User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"])
data data
|> Map.put("context", context) |> Map.put("context", context)
|> Map.put("context_id", context_id) |> Map.put("context_id", context_id)
|> cast_and_filter_recipients("to", follower_collection)
|> cast_and_filter_recipients("cc", follower_collection)
|> cast_and_filter_recipients("bto", follower_collection)
|> cast_and_filter_recipients("bcc", follower_collection)
|> Transmogrifier.fix_implicit_addressing(follower_collection)
end end
def fix_attribution(data) do def fix_activity_addressing(activity, _meta) do
data %User{follower_address: follower_collection} = User.get_cached_by_ap_id(activity["actor"])
|> Map.put_new("actor", data["attributedTo"])
activity
|> cast_and_filter_recipients("to", follower_collection)
|> cast_and_filter_recipients("cc", follower_collection)
|> cast_and_filter_recipients("bto", follower_collection)
|> cast_and_filter_recipients("bcc", follower_collection)
|> Transmogrifier.fix_implicit_addressing(follower_collection)
end end
def fix_actor(data) do def fix_actor(data) do
actor = Containment.get_actor(data) actor =
data
|> Map.put_new("actor", data["attributedTo"])
|> Containment.get_actor()
data data
|> Map.put("actor", actor) |> Map.put("actor", actor)

View file

@ -9,11 +9,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
@spec validate_any_presence(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()
def validate_any_presence(cng, fields) do def validate_any_presence(cng, fields) do
non_empty = non_empty =
fields fields
|> Enum.map(fn field -> get_field(cng, field) end) |> Enum.map(fn field -> get_field(cng, field) end)
|> Enum.any?(fn |> Enum.any?(fn
nil -> false
[] -> false [] -> false
_ -> true _ -> true
end) end)
@ -29,6 +31,7 @@ def validate_any_presence(cng, fields) do
end end
end end
@spec validate_actor_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
def validate_actor_presence(cng, options \\ []) do def validate_actor_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :actor) field_name = Keyword.get(options, :field_name, :actor)
@ -47,6 +50,7 @@ def validate_actor_presence(cng, options \\ []) do
end) end)
end end
@spec validate_object_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
def validate_object_presence(cng, options \\ []) do def validate_object_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :object) field_name = Keyword.get(options, :field_name, :object)
allowed_types = Keyword.get(options, :allowed_types, false) allowed_types = Keyword.get(options, :allowed_types, false)
@ -68,6 +72,7 @@ def validate_object_presence(cng, options \\ []) do
end) end)
end end
@spec validate_object_or_user_presence(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
def validate_object_or_user_presence(cng, options \\ []) do def validate_object_or_user_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :object) field_name = Keyword.get(options, :field_name, :object)
options = Keyword.put(options, :field_name, field_name) options = Keyword.put(options, :field_name, field_name)
@ -83,6 +88,7 @@ def validate_object_or_user_presence(cng, options \\ []) do
if actor_cng.valid?, do: actor_cng, else: object_cng if actor_cng.valid?, do: actor_cng, else: object_cng
end end
@spec validate_host_match(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()
def validate_host_match(cng, fields \\ [:id, :actor]) do def validate_host_match(cng, fields \\ [:id, :actor]) do
if same_domain?(cng, fields) do if same_domain?(cng, fields) do
cng cng
@ -95,6 +101,7 @@ def validate_host_match(cng, fields \\ [:id, :actor]) do
end end
end end
@spec validate_fields_match(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t()
def validate_fields_match(cng, fields) do def validate_fields_match(cng, fields) do
if map_unique?(cng, fields) do if map_unique?(cng, fields) do
cng cng
@ -122,12 +129,14 @@ defp map_unique?(cng, fields, func \\ & &1) do
end) end)
end end
@spec same_domain?(Ecto.Changeset.t(), [atom()]) :: boolean()
def same_domain?(cng, fields \\ [:actor, :object]) do def same_domain?(cng, fields \\ [:actor, :object]) do
map_unique?(cng, fields, fn value -> URI.parse(value).host end) map_unique?(cng, fields, fn value -> URI.parse(value).host end)
end end
# This figures out if a user is able to create, delete or modify something # This figures out if a user is able to create, delete or modify something
# based on the domain and superuser status # based on the domain and superuser status
@spec validate_modification_rights(Ecto.Changeset.t()) :: Ecto.Changeset.t()
def validate_modification_rights(cng) do def validate_modification_rights(cng) do
actor = User.get_cached_by_ap_id(get_field(cng, :actor)) actor = User.get_cached_by_ap_id(get_field(cng, :actor))

View file

@ -39,7 +39,7 @@ def cast_and_validate(data, meta \\ []) do
|> validate_data(meta) |> validate_data(meta)
end end
def validate_data(cng, meta \\ []) do defp validate_data(cng, meta) do
cng cng
|> validate_required([:id, :actor, :to, :type, :object]) |> validate_required([:id, :actor, :to, :type, :object])
|> validate_inclusion(:type, ["Create"]) |> validate_inclusion(:type, ["Create"])

View file

@ -10,8 +10,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.Transmogrifier
import Ecto.Changeset import Ecto.Changeset
@ -23,6 +25,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
field(:type, :string) field(:type, :string)
field(:to, ObjectValidators.Recipients, default: []) field(:to, ObjectValidators.Recipients, default: [])
field(:cc, ObjectValidators.Recipients, default: []) field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: [])
field(:object, ObjectValidators.ObjectID) field(:object, ObjectValidators.ObjectID)
field(:expires_at, ObjectValidators.DateTime) field(:expires_at, ObjectValidators.DateTime)
@ -54,39 +58,37 @@ def changeset(struct, data) do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
defp fix_context(data, meta) do # CommonFixes.fix_activity_addressing adapted for Create specific behavior
if object = meta[:object_data] do defp fix_addressing(data, object) do
Map.put_new(data, "context", object["context"]) %User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["actor"])
else
data
end
end
defp fix_addressing(data, meta) do
if object = meta[:object_data] do
data
|> Map.put_new("to", object["to"] || [])
|> Map.put_new("cc", object["cc"] || [])
else
data
end
end
defp fix(data, meta) do
data data
|> fix_context(meta) |> CommonFixes.cast_and_filter_recipients("to", follower_collection, object["to"])
|> fix_addressing(meta) |> CommonFixes.cast_and_filter_recipients("cc", follower_collection, object["cc"])
|> CommonFixes.fix_actor() |> CommonFixes.cast_and_filter_recipients("bto", follower_collection, object["bto"])
|> CommonFixes.cast_and_filter_recipients("bcc", follower_collection, object["bcc"])
|> Transmogrifier.fix_implicit_addressing(follower_collection)
end end
def validate_data(cng, meta \\ []) do def fix(data, meta) do
object = meta[:object_data]
data
|> CommonFixes.fix_actor()
|> Map.put_new("context", object["context"])
|> fix_addressing(object)
end
defp validate_data(cng, meta) do
object = meta[:object_data]
cng cng
|> validate_required([:actor, :type, :object]) |> validate_required([:actor, :type, :object, :to, :cc])
|> validate_inclusion(:type, ["Create"]) |> validate_inclusion(:type, ["Create"])
|> CommonValidations.validate_actor_presence() |> CommonValidations.validate_actor_presence()
|> CommonValidations.validate_any_presence([:to, :cc]) |> validate_actors_match(object)
|> validate_actors_match(meta) |> validate_context_match(object)
|> validate_context_match(meta) |> validate_addressing_match(object)
|> validate_object_nonexistence() |> validate_object_nonexistence()
|> validate_object_containment() |> validate_object_containment()
end end
@ -118,8 +120,8 @@ def validate_object_nonexistence(cng) do
end) end)
end end
def validate_actors_match(cng, meta) do def validate_actors_match(cng, object) do
attributed_to = meta[:object_data]["attributedTo"] || meta[:object_data]["actor"] attributed_to = object["attributedTo"] || object["actor"]
cng cng
|> validate_change(:actor, fn :actor, actor -> |> validate_change(:actor, fn :actor, actor ->
@ -131,7 +133,7 @@ def validate_actors_match(cng, meta) do
end) end)
end end
def validate_context_match(cng, %{object_data: %{"context" => object_context}}) do def validate_context_match(cng, %{"context" => object_context}) do
cng cng
|> validate_change(:context, fn :context, context -> |> validate_change(:context, fn :context, context ->
if context == object_context do if context == object_context do
@ -142,5 +144,18 @@ def validate_context_match(cng, %{object_data: %{"context" => object_context}})
end) end)
end end
def validate_context_match(cng, _), do: cng def validate_addressing_match(cng, object) do
[:to, :cc, :bcc, :bto]
|> Enum.reduce(cng, fn field, cng ->
object_data = object[to_string(field)]
validate_change(cng, field, fn field, data ->
if data == object_data do
[]
else
[{field, "field doesn't match with object (#{inspect(object_data)})"}]
end
end)
end)
end
end end

View file

@ -1,29 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:actor, ObjectValidators.ObjectID)
field(:type, :string)
field(:to, ObjectValidators.Recipients, default: [])
field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: [])
embeds_one(:object, NoteValidator)
end
def cast_data(data) do
cast(%__MODULE__{}, data, __schema__(:fields))
end
end

View file

@ -53,7 +53,7 @@ def add_deleted_activity_id(cng) do
Tombstone Tombstone
Video Video
} }
def validate_data(cng) do defp validate_data(cng) do
cng cng
|> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Delete"]) |> validate_inclusion(:type, ["Delete"])

View file

@ -70,7 +70,7 @@ def validate_emoji(cng) do
end end
end end
def validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["EmojiReact"]) |> validate_inclusion(:type, ["EmojiReact"])
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content]) |> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content])

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
import Ecto.Changeset import Ecto.Changeset
@ -23,8 +24,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
field(:cc, ObjectValidators.Recipients, default: []) field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: []) field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: []) field(:bcc, ObjectValidators.Recipients, default: [])
# TODO: Write type embeds_many(:tag, TagValidator)
field(:tag, {:array, :map}, default: [])
field(:type, :string) field(:type, :string)
field(:name, :string) field(:name, :string)
@ -72,8 +72,8 @@ def cast_data(data) do
defp fix(data) do defp fix(data) do
data data
|> CommonFixes.fix_defaults() |> CommonFixes.fix_actor()
|> CommonFixes.fix_attribution() |> CommonFixes.fix_object_defaults()
|> Transmogrifier.fix_emoji() |> Transmogrifier.fix_emoji()
end end
@ -81,11 +81,12 @@ def changeset(struct, data) do
data = fix(data) data = fix(data)
struct struct
|> cast(data, __schema__(:fields) -- [:attachment]) |> cast(data, __schema__(:fields) -- [:attachment, :tag])
|> cast_embed(:attachment) |> cast_embed(:attachment)
|> cast_embed(:tag)
end end
def validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Event"]) |> validate_inclusion(:type, ["Event"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])

View file

@ -27,7 +27,7 @@ def cast_data(data) do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(cng) do defp validate_data(cng) do
cng cng
|> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Follow"]) |> validate_inclusion(:type, ["Follow"])

View file

@ -76,7 +76,7 @@ def fix_recipients(cng) do
end end
end end
def validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Like"]) |> validate_inclusion(:type, ["Like"])
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc]) |> validate_required([:id, :type, :object, :actor, :context, :to, :cc])

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
import Ecto.Changeset import Ecto.Changeset
@ -24,8 +25,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
field(:cc, ObjectValidators.Recipients, default: []) field(:cc, ObjectValidators.Recipients, default: [])
field(:bto, ObjectValidators.Recipients, default: []) field(:bto, ObjectValidators.Recipients, default: [])
field(:bcc, ObjectValidators.Recipients, default: []) field(:bcc, ObjectValidators.Recipients, default: [])
# TODO: Write type embeds_many(:tag, TagValidator)
field(:tag, {:array, :map}, default: [])
field(:type, :string) field(:type, :string)
field(:content, :string) field(:content, :string)
field(:context, :string) field(:context, :string)
@ -83,8 +83,8 @@ defp fix_closed(data) do
defp fix(data) do defp fix(data) do
data data
|> CommonFixes.fix_defaults() |> CommonFixes.fix_actor()
|> CommonFixes.fix_attribution() |> CommonFixes.fix_object_defaults()
|> Transmogrifier.fix_emoji() |> Transmogrifier.fix_emoji()
|> fix_closed() |> fix_closed()
end end
@ -93,13 +93,14 @@ def changeset(struct, data) do
data = fix(data) data = fix(data)
struct struct
|> cast(data, __schema__(:fields) -- [:anyOf, :oneOf, :attachment]) |> cast(data, __schema__(:fields) -- [:anyOf, :oneOf, :attachment, :tag])
|> cast_embed(:attachment) |> cast_embed(:attachment)
|> cast_embed(:anyOf) |> cast_embed(:anyOf)
|> cast_embed(:oneOf) |> cast_embed(:oneOf)
|> cast_embed(:tag)
end end
def validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Question"]) |> validate_inclusion(:type, ["Question"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])

View file

@ -0,0 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
import Ecto.Changeset
@primary_key false
embedded_schema do
# Common
field(:type, :string)
field(:name, :string)
# Mention, Hashtag
field(:href, ObjectValidators.Uri)
# Emoji
embeds_one :icon, IconObjectValidator, primary_key: false do
field(:type, :string)
field(:url, ObjectValidators.Uri)
end
field(:updated, ObjectValidators.DateTime)
field(:id, ObjectValidators.Uri)
end
def cast_and_validate(data) do
data
|> cast_data()
end
def cast_data(data) do
%__MODULE__{}
|> changeset(data)
end
def changeset(struct, %{"type" => "Mention"} = data) do
struct
|> cast(data, [:type, :name, :href])
|> validate_required([:type, :href])
end
def changeset(struct, %{"type" => "Hashtag", "name" => name} = data) do
name =
cond do
"#" <> name -> name
name -> name
end
|> String.downcase()
data = Map.put(data, "name", name)
struct
|> cast(data, [:type, :name, :href])
|> validate_required([:type, :name])
end
def changeset(struct, %{"type" => "Emoji"} = data) do
data = Map.put(data, "name", String.trim(data["name"], ":"))
struct
|> cast(data, [:type, :name, :updated, :id])
|> cast_embed(:icon, with: &icon_changeset/2)
|> validate_required([:type, :name, :icon])
end
def icon_changeset(struct, data) do
struct
|> cast(data, [:type, :url])
|> validate_inclusion(:type, ~w[Image])
|> validate_required([:type, :url])
end
end

View file

@ -38,7 +38,7 @@ def changeset(struct, data) do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(data_cng) do defp validate_data(data_cng) do
data_cng data_cng
|> validate_inclusion(:type, ["Undo"]) |> validate_inclusion(:type, ["Undo"])
|> validate_required([:id, :type, :object, :actor, :to, :cc]) |> validate_required([:id, :type, :object, :actor, :to, :cc])

View file

@ -28,7 +28,7 @@ def cast_data(data) do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(cng) do defp validate_data(cng) do
cng cng
|> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Update"]) |> validate_inclusion(:type, ["Update"])

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Utils
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidator
@ -14,19 +15,19 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
@side_effects Config.get([:pipeline, :side_effects], SideEffects) defp side_effects, do: Config.get([:pipeline, :side_effects], SideEffects)
@federator Config.get([:pipeline, :federator], Federator) defp federator, do: Config.get([:pipeline, :federator], Federator)
@object_validator Config.get([:pipeline, :object_validator], ObjectValidator) defp object_validator, do: Config.get([:pipeline, :object_validator], ObjectValidator)
@mrf Config.get([:pipeline, :mrf], MRF) defp mrf, do: Config.get([:pipeline, :mrf], MRF)
@activity_pub Config.get([:pipeline, :activity_pub], ActivityPub) defp activity_pub, do: Config.get([:pipeline, :activity_pub], ActivityPub)
@config Config.get([:pipeline, :config], Config) defp config, do: Config.get([:pipeline, :config], Config)
@spec common_pipeline(map(), keyword()) :: @spec common_pipeline(map(), keyword()) ::
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()} {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do
{:ok, {:ok, activity, meta}} -> {:ok, {:ok, activity, meta}} ->
@side_effects.handle_after_transaction(meta) side_effects().handle_after_transaction(meta)
{:ok, activity, meta} {:ok, activity, meta}
{:ok, value} -> {:ok, value} ->
@ -40,19 +41,17 @@ def common_pipeline(object, meta) do
end end
end end
def do_common_pipeline(object, meta) do def do_common_pipeline(%{__struct__: _}, _meta), do: {:error, :is_struct}
with {_, {:ok, validated_object, meta}} <-
{:validate_object, @object_validator.validate(object, meta)}, def do_common_pipeline(message, meta) do
{_, {:ok, mrfd_object, meta}} <- with {_, {:ok, message, meta}} <- {:validate, object_validator().validate(message, meta)},
{:mrf_object, @mrf.pipeline_filter(validated_object, meta)}, {_, {:ok, message, meta}} <- {:mrf, mrf().pipeline_filter(message, meta)},
{_, {:ok, activity, meta}} <- {_, {:ok, message, meta}} <- {:persist, activity_pub().persist(message, meta)},
{:persist_object, @activity_pub.persist(mrfd_object, meta)}, {_, {:ok, message, meta}} <- {:side_effects, side_effects().handle(message, meta)},
{_, {:ok, activity, meta}} <- {_, {:ok, _}} <- {:federation, maybe_federate(message, meta)} do
{:execute_side_effects, @side_effects.handle(activity, meta)}, {:ok, message, meta}
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta}
else else
{:mrf_object, {:reject, message, _}} -> {:reject, message} {:mrf, {:reject, message, _}} -> {:reject, message}
e -> {:error, e} e -> {:error, e}
end end
end end
@ -61,7 +60,7 @@ defp maybe_federate(%Object{}, _), do: {:ok, :not_federated}
defp maybe_federate(%Activity{} = activity, meta) do defp maybe_federate(%Activity{} = activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do with {:ok, local} <- Keyword.fetch(meta, :local) do
do_not_federate = meta[:do_not_federate] || !@config.get([:instance, :federating]) do_not_federate = meta[:do_not_federate] || !config().get([:instance, :federating])
if !do_not_federate and local and not Visibility.is_local_public?(activity) do if !do_not_federate and local and not Visibility.is_local_public?(activity) do
activity = activity =
@ -71,7 +70,7 @@ defp maybe_federate(%Activity{} = activity, meta) do
activity activity
end end
@federator.publish(activity) federator().publish(activity)
{:ok, :federated} {:ok, :federated}
else else
{:ok, :not_federated} {:ok, :not_federated}

View file

@ -272,7 +272,7 @@ def gather_webfinger_links(%User{} = user) do
}, },
%{ %{
"rel" => "http://ostatus.org/schema/1.0/subscribe", "rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => "#{Pleroma.Web.base_url()}/ostatus_subscribe?acct={uri}" "template" => "#{Pleroma.Web.Endpoint.url()}/ostatus_subscribe?acct={uri}"
} }
] ]
end end

View file

@ -203,6 +203,19 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
Object.increase_replies_count(in_reply_to) Object.increase_replies_count(in_reply_to)
end end
reply_depth = (meta[:depth] || 0) + 1
# FIXME: Force inReplyTo to replies
if Pleroma.Web.Federator.allowed_thread_distance?(reply_depth) and
object.data["replies"] != nil do
for reply_id <- object.data["replies"] do
Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
"id" => reply_id,
"depth" => reply_depth
})
end
end
ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn -> ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end) Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
end) end)
@ -276,10 +289,10 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
result = result =
case deleted_object do case deleted_object do
%Object{} -> %Object{} ->
with {:ok, deleted_object, activity} <- Object.delete(deleted_object), with {:ok, deleted_object, _activity} <- Object.delete(deleted_object),
{_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]}, {_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]},
%User{} = user <- User.get_cached_by_ap_id(actor) do %User{} = user <- User.get_cached_by_ap_id(actor) do
User.remove_pinnned_activity(user, activity) User.remove_pinned_object_id(user, deleted_object.data["id"])
{:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object) {:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)
@ -312,6 +325,63 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
end end
end end
# Tasks this handles:
# - adds pin to user
# - removes expiration job for pinned activity, if was set for expiration
@impl true
def handle(%{data: %{"type" => "Add"} = data} = object, meta) do
with %User{} = user <- User.get_cached_by_ap_id(data["actor"]),
{:ok, _user} <- User.add_pinned_object_id(user, data["object"]) do
# if pinned activity was scheduled for deletion, we remove job
if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(meta[:activity_id]) do
Oban.cancel_job(expiration.id)
end
{:ok, object, meta}
else
nil ->
{:error, :user_not_found}
{:error, changeset} ->
if changeset.errors[:pinned_objects] do
{:error, :pinned_statuses_limit_reached}
else
changeset.errors
end
end
end
# Tasks this handles:
# - removes pin from user
# - removes corresponding Add activity
# - if activity had expiration, recreates activity expiration job
@impl true
def handle(%{data: %{"type" => "Remove"} = data} = object, meta) do
with %User{} = user <- User.get_cached_by_ap_id(data["actor"]),
{:ok, _user} <- User.remove_pinned_object_id(user, data["object"]) do
data["object"]
|> Activity.add_by_params_query(user.ap_id, user.featured_address)
|> Repo.delete_all()
# if pinned activity was scheduled for deletion, we reschedule it for deletion
if meta[:expires_at] do
# MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation
{:ok, expires_at} =
Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast(meta[:expires_at])
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
activity_id: meta[:activity_id],
expires_at: expires_at
})
end
{:ok, object, meta}
else
nil -> {:error, :user_not_found}
error -> error
end
end
# Nothing to do # Nothing to do
@impl true @impl true
def handle(object, meta) do def handle(object, meta) do
@ -366,7 +436,7 @@ def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do
end end
def handle_object_creation(%{"type" => objtype} = object, meta) def handle_object_creation(%{"type" => objtype} = object, meta)
when objtype in ~w[Audio Video Question Event Article] do when objtype in ~w[Audio Video Question Event Article Note] do
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
{:ok, object, meta} {:ok, object, meta}
end end

View file

@ -43,7 +43,6 @@ def fix_object(object, options \\ []) do
|> fix_content_map() |> fix_content_map()
|> fix_addressing() |> fix_addressing()
|> fix_summary() |> fix_summary()
|> fix_type(options)
end end
def fix_summary(%{"summary" => nil} = object) do def fix_summary(%{"summary" => nil} = object) do
@ -72,17 +71,21 @@ def fix_addressing_list(map, field) do
end end
end end
def fix_explicit_addressing( # if directMessage flag is set to true, leave the addressing alone
%{"to" => to, "cc" => cc} = object, def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection),
explicit_mentions, do: object
follower_collection
) do
explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, follower_collection) do
explicit_mentions =
Utils.determine_explicit_mentions(object) ++
[Pleroma.Constants.as_public(), follower_collection]
explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end) explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
final_cc = final_cc =
(cc ++ explicit_cc) (cc ++ explicit_cc)
|> Enum.filter(& &1)
|> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end) |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
|> Enum.uniq() |> Enum.uniq()
@ -91,29 +94,6 @@ def fix_explicit_addressing(
|> Map.put("cc", final_cc) |> Map.put("cc", final_cc)
end end
def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
# if directMessage flag is set to true, leave the addressing alone
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
def fix_explicit_addressing(object) do
explicit_mentions = Utils.determine_explicit_mentions(object)
%User{follower_address: follower_collection} =
object
|> Containment.get_actor()
|> User.get_cached_by_ap_id()
explicit_mentions =
explicit_mentions ++
[
Pleroma.Constants.as_public(),
follower_collection
]
fix_explicit_addressing(object, explicit_mentions, follower_collection)
end
# if as:Public is addressed, then make sure the followers collection is also addressed # if as:Public is addressed, then make sure the followers collection is also addressed
# so that the activities will be delivered to local users. # so that the activities will be delivered to local users.
def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
@ -137,19 +117,19 @@ def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collec
end end
end end
def fix_implicit_addressing(object, _), do: object
def fix_addressing(object) do def fix_addressing(object) do
{:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"]) {:ok, %User{follower_address: follower_collection}} =
followers_collection = User.ap_followers(user) object
|> Containment.get_actor()
|> User.get_or_fetch_by_ap_id()
object object
|> fix_addressing_list("to") |> fix_addressing_list("to")
|> fix_addressing_list("cc") |> fix_addressing_list("cc")
|> fix_addressing_list("bto") |> fix_addressing_list("bto")
|> fix_addressing_list("bcc") |> fix_addressing_list("bcc")
|> fix_explicit_addressing() |> fix_explicit_addressing(follower_collection)
|> fix_implicit_addressing(followers_collection) |> fix_implicit_addressing(follower_collection)
end end
def fix_actor(%{"attributedTo" => actor} = object) do def fix_actor(%{"attributedTo" => actor} = object) do
@ -223,10 +203,17 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
media_type = media_type =
cond do cond do
is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"] is_map(url) && MIME.extensions(url["mediaType"]) != [] ->
MIME.valid?(data["mediaType"]) -> data["mediaType"] url["mediaType"]
MIME.valid?(data["mimeType"]) -> data["mimeType"]
true -> nil is_bitstring(data["mediaType"]) && MIME.extensions(data["mediaType"]) != [] ->
data["mediaType"]
is_bitstring(data["mimeType"]) && MIME.extensions(data["mimeType"]) != [] ->
data["mimeType"]
true ->
nil
end end
href = href =
@ -244,6 +231,8 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
"type" => Map.get(url || %{}, "type", "Link") "type" => Map.get(url || %{}, "type", "Link")
} }
|> Maps.put_if_present("mediaType", media_type) |> Maps.put_if_present("mediaType", media_type)
|> Maps.put_if_present("width", (url || %{})["width"] || data["width"])
|> Maps.put_if_present("height", (url || %{})["height"] || data["height"])
%{ %{
"url" => [attachment_url], "url" => [attachment_url],
@ -340,19 +329,18 @@ def fix_content_map(%{"contentMap" => content_map} = object) do
def fix_content_map(object), do: object def fix_content_map(object), do: object
def fix_type(object, options \\ []) defp fix_type(%{"type" => "Note", "inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
options = Keyword.put(options, :fetch, true)
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options) with %Object{data: %{"type" => "Question"}} <- Object.normalize(reply_id, options) do
when is_binary(reply_id) do
with true <- Federator.allowed_thread_distance?(options[:depth]),
{:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
Map.put(object, "type", "Answer") Map.put(object, "type", "Answer")
else else
_ -> object _ -> object
end end
end end
def fix_type(object, _), do: object defp fix_type(object, _options), do: object
# Reduce the object list to find the reported user. # Reduce the object list to find the reported user.
defp get_reported(objects) do defp get_reported(objects) do
@ -423,10 +411,9 @@ def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id
# - tags # - tags
# - emoji # - emoji
def handle_incoming( def handle_incoming(
%{"type" => "Create", "object" => %{"type" => objtype} = object} = data, %{"type" => "Create", "object" => %{"type" => "Page"} = object} = data,
options options
) ) do
when objtype in ~w{Note Page} do
actor = Containment.get_actor(data) actor = Containment.get_actor(data)
with nil <- Activity.get_create_by_object_ap_id(object["id"]), with nil <- Activity.get_create_by_object_ap_id(object["id"]),
@ -518,14 +505,23 @@ def handle_incoming(
def handle_incoming( def handle_incoming(
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
_options options
) )
when objtype in ~w{Question Answer ChatMessage Audio Video Event Article} do when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note} do
data = Map.put(data, "object", strip_internal_fields(data["object"])) fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
object =
data["object"]
|> strip_internal_fields()
|> fix_type(fetch_options)
|> fix_in_reply_to(fetch_options)
data = Map.put(data, "object", object)
options = Keyword.put(options, :local, false)
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
nil <- Activity.get_create_by_object_ap_id(obj_id), nil <- Activity.get_create_by_object_ap_id(obj_id),
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity, _} <- Pipeline.common_pipeline(data, options) do
{:ok, activity} {:ok, activity}
else else
%Activity{} = activity -> {:ok, activity} %Activity{} = activity -> {:ok, activity}
@ -534,7 +530,7 @@ def handle_incoming(
end end
def handle_incoming(%{"type" => type} = data, _options) def handle_incoming(%{"type" => type} = data, _options)
when type in ~w{Like EmojiReact Announce} do when type in ~w{Like EmojiReact Announce Add Remove} 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
@ -564,7 +560,7 @@ def handle_incoming(
Pipeline.common_pipeline(data, local: false) do Pipeline.common_pipeline(data, local: false) do
{:ok, activity} {:ok, activity}
else else
{:error, {:validate_object, _}} = e -> {:error, {:validate, _}} = e ->
# Check if we have a create activity for this # Check if we have a create activity for this
with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]), with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
%Activity{data: %{"actor" => actor}} <- %Activity{data: %{"actor" => actor}} <-
@ -949,7 +945,7 @@ def prepare_attachments(object) do
object object
|> Map.get("attachment", []) |> Map.get("attachment", [])
|> Enum.map(fn data -> |> Enum.map(fn data ->
[%{"mediaType" => media_type, "href" => href} | _] = data["url"] [%{"mediaType" => media_type, "href" => href} = url | _] = data["url"]
%{ %{
"url" => href, "url" => href,
@ -957,6 +953,9 @@ def prepare_attachments(object) do
"name" => data["name"], "name" => data["name"],
"type" => "Document" "type" => "Document"
} }
|> Maps.put_if_present("width", url["width"])
|> Maps.put_if_present("height", url["height"])
|> Maps.put_if_present("blurhash", data["blurhash"])
end) end)
Map.put(object, "attachment", attachments) Map.put(object, "attachment", attachments)
@ -1000,6 +999,7 @@ def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id), with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id), {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
{:ok, user} <- update_user(user, data) do {:ok, user} <- update_user(user, data) do
{:ok, _pid} = Task.start(fn -> ActivityPub.pinned_fetch_task(user) end)
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id}) TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
{:ok, user} {:ok, user}
else else

View file

@ -12,7 +12,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
@ -38,6 +37,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@supported_report_states ~w(open closed resolved) @supported_report_states ~w(open closed resolved)
@valid_visibilities ~w(public unlisted private direct) @valid_visibilities ~w(public unlisted private direct)
def as_local_public, do: Endpoint.url() <> "/#Public"
# Some implementations send the actor URI as the actor field, others send the entire actor object, # Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have. # so figure out what the actor's URI is based on what we have.
def get_ap_id(%{"id" => id} = _), do: id def get_ap_id(%{"id" => id} = _), do: id
@ -96,8 +97,11 @@ def maybe_splice_recipient(ap_id, params) do
!label_in_collection?(ap_id, params["cc"]) !label_in_collection?(ap_id, params["cc"])
if need_splice? do if need_splice? do
cc_list = extract_list(params["cc"]) cc = [ap_id | extract_list(params["cc"])]
Map.put(params, "cc", [ap_id | cc_list])
params
|> Map.put("cc", cc)
|> Maps.safe_put_in(["object", "cc"], cc)
else else
params params
end end
@ -107,7 +111,7 @@ def make_json_ld_header do
%{ %{
"@context" => [ "@context" => [
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"#{Web.base_url()}/schemas/litepub-0.1.jsonld", "#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
%{ %{
"@language" => "und" "@language" => "und"
} }
@ -132,7 +136,7 @@ def generate_object_id do
end end
def generate_id(type) do def generate_id(type) do
"#{Web.base_url()}/#{type}/#{UUID.generate()}" "#{Endpoint.url()}/#{type}/#{UUID.generate()}"
end end
def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do

View file

@ -6,8 +6,10 @@ defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Keys alias Pleroma.Keys
alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
@ -97,6 +99,7 @@ def render("user.json", %{user: user}) do
"followers" => "#{user.ap_id}/followers", "followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox", "inbox" => "#{user.ap_id}/inbox",
"outbox" => "#{user.ap_id}/outbox", "outbox" => "#{user.ap_id}/outbox",
"featured" => "#{user.ap_id}/collections/featured",
"preferredUsername" => user.nickname, "preferredUsername" => user.nickname,
"name" => user.name, "name" => user.name,
"summary" => user.bio, "summary" => user.bio,
@ -245,6 +248,25 @@ def render("activity_collection_page.json", %{
|> Map.merge(pagination) |> Map.merge(pagination)
end end
def render("featured.json", %{
user: %{featured_address: featured_address, pinned_objects: pinned_objects}
}) do
objects =
pinned_objects
|> Enum.sort_by(fn {_, pinned_at} -> pinned_at end, &>=/2)
|> Enum.map(fn {id, _} ->
ObjectView.render("object.json", %{object: Object.get_cached_by_ap_id(id)})
end)
%{
"id" => featured_address,
"type" => "OrderedCollection",
"orderedItems" => objects,
"totalItems" => length(objects)
}
|> Map.merge(Utils.make_json_ld_header())
end
defp maybe_put_total_items(map, false, _total), do: map defp maybe_put_total_items(map, false, _total), do: map
defp maybe_put_total_items(map, true, total) do defp maybe_put_total_items(map, true, total) do

View file

@ -20,14 +20,14 @@ def is_public?(%{"directMessage" => true}), do: false
def is_public?(data) do def is_public?(data) do
Utils.label_in_message?(Pleroma.Constants.as_public(), data) or Utils.label_in_message?(Pleroma.Constants.as_public(), data) or
Utils.label_in_message?(Pleroma.Constants.as_local_public(), data) Utils.label_in_message?(Utils.as_local_public(), data)
end end
def is_local_public?(%Object{data: data}), do: is_local_public?(data) def is_local_public?(%Object{data: data}), do: is_local_public?(data)
def is_local_public?(%Activity{data: data}), do: is_local_public?(data) def is_local_public?(%Activity{data: data}), do: is_local_public?(data)
def is_local_public?(data) do def is_local_public?(data) do
Utils.label_in_message?(Pleroma.Constants.as_local_public(), data) and Utils.label_in_message?(Utils.as_local_public(), data) and
not Utils.label_in_message?(Pleroma.Constants.as_public(), data) not Utils.label_in_message?(Pleroma.Constants.as_public(), data)
end end
@ -127,7 +127,7 @@ def get_visibility(object) do
Pleroma.Constants.as_public() in cc -> Pleroma.Constants.as_public() in cc ->
"unlisted" "unlisted"
Pleroma.Constants.as_local_public() in to -> Utils.as_local_public() in to ->
"local" "local"
# this should use the sql for the object's activity # this should use the sql for the object's activity

View file

@ -13,7 +13,6 @@ defmodule Pleroma.Web.AdminAPI.OAuthAppController do
require Logger require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:put_view, Pleroma.Web.MastodonAPI.AppView)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,

View file

@ -0,0 +1,10 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.OAuthAppView do
use Pleroma.Web, :view
alias Pleroma.Web.MastodonAPI
def render(view, opts), do: MastodonAPI.AppView.render(view, opts)
end

View file

@ -105,6 +105,7 @@ def show_operation do
responses: %{ responses: %{
200 => Operation.response("Media", "application/json", Attachment), 200 => Operation.response("Media", "application/json", Attachment),
401 => Operation.response("Media", "application/json", ApiError), 401 => Operation.response("Media", "application/json", ApiError),
403 => Operation.response("Media", "application/json", ApiError),
422 => Operation.response("Media", "application/json", ApiError) 422 => Operation.response("Media", "application/json", ApiError)
} }
} }

View file

@ -182,7 +182,34 @@ def pin_operation do
parameters: [id_param()], parameters: [id_param()],
responses: %{ responses: %{
200 => status_response(), 200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError) 400 =>
Operation.response("Bad Request", "application/json", %Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "You have already pinned the maximum number of statuses"
}
}),
404 =>
Operation.response("Not found", "application/json", %Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "Record not found"
}
}),
422 =>
Operation.response(
"Unprocessable Entity",
"application/json",
%Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "Someone else's status cannot be pinned"
}
}
)
} }
} }
end end
@ -197,7 +224,22 @@ def unpin_operation do
parameters: [id_param()], parameters: [id_param()],
responses: %{ responses: %{
200 => status_response(), 200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError) 400 =>
Operation.response("Bad Request", "application/json", %Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "You have already pinned the maximum number of statuses"
}
}),
404 =>
Operation.response("Not found", "application/json", %Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "Record not found"
}
})
} }
} }
end end

View file

@ -115,7 +115,8 @@ def hashtag_operation do
], ],
operationId: "TimelineController.hashtag", operationId: "TimelineController.hashtag",
responses: %{ responses: %{
200 => Operation.response("Array of Status", "application/json", array_of_statuses()) 200 => Operation.response("Array of Status", "application/json", array_of_statuses()),
401 => Operation.response("Error", "application/json", ApiError)
} }
} }
end end

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