Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into simplePolicy_reasons_for_instance_specific_policies

This commit is contained in:
Ilja 2021-01-09 17:28:58 +01:00
commit 95d5589df6
319 changed files with 2493 additions and 1285 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
/db /db
/deps /deps
/*.ez /*.ez
/test/instance
/test/uploads /test/uploads
/.elixir_ls /.elixir_ls
/test/fixtures/DSCN0010_tmp.jpg /test/fixtures/DSCN0010_tmp.jpg

View File

@ -10,24 +10,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Polls now always return a `voters_count`, even if they are single-choice. - Polls now always return a `voters_count`, even if they are single-choice.
- Admin Emails: The ap id is used as the user link in emails now. - Admin Emails: The ap id is used as the user link in emails now.
- Improved registration workflow for email confirmation and account approval modes.
- **Breaking:** Changed `mix pleroma.user toggle_confirmed` to `mix pleroma.user confirm`
- Search: When using Postgres 11+, Pleroma will use the `websearch_to_tsvector` function to parse search queries. - Search: When using Postgres 11+, Pleroma will use the `websearch_to_tsvector` function to parse search queries.
- Emoji: Support the full Unicode 13.1 set of Emoji for reactions, plus regional indicators. - Emoji: Support the full Unicode 13.1 set of Emoji for reactions, plus regional indicators.
- **Breaking** Besides only the instance, entries for simple_policy, transparency_exclusions and quarantined_instances now contain a reason as well. - **Breaking** Besides only the instance, entries for simple_policy, transparency_exclusions and
- Admin API: Reports now ordered by newest
### Added ### Added
- Reports now generate notifications for admins and mods. - Reports now generate notifications for admins and mods.
- Experimental websocket-based federation between Pleroma instances. - Experimental websocket-based federation between Pleroma instances.
- Support for local-only statuses - Support for local-only statuses.
- Support pagination of blocks and mutes. - Support pagination of blocks and mutes.
- Account backup. - Account backup.
- Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance. - Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance.
- Ability to view remote timelines, with ex. `/api/v1/timelines/public?instance=lain.com` and streams `public:remote` and `public:remote:media`. - Ability to view remote timelines, with ex. `/api/v1/timelines/public?instance=lain.com` and streams `public:remote` and `public:remote:media`.
- The site title is now injected as a `title` tag like preloads or metadata. - The site title is now injected as a `title` tag like preloads or metadata.
- Password reset tokens now are not accepted after a certain age. - Password reset tokens now are not accepted after a certain age.
- Mix tasks to help with displaying and removing ConfigDB entries. See `mix pleroma.config` - Mix tasks to help with displaying and removing ConfigDB entries. See `mix pleroma.config`.
- OAuth form improvements: users are remembered by their cookie, the CSS is overridable by the admin, and the style has been improved. - OAuth form improvements: users are remembered by their cookie, the CSS is overridable by the admin, and the style has been improved.
- OAuth improvements and fixes: more secure session-based authentication (by token that could be revoked anytime), ability to revoke belonging OAuth token from any client etc. - OAuth improvements and fixes: more secure session-based authentication (by token that could be revoked anytime), ability to revoke belonging OAuth token from any client etc.
- Ability to set ActivityPub aliases for follower migration.
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
@ -35,13 +39,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma API: Add `idempotency_key` to the chat message entity that can be used for optimistic message sending. - Pleroma API: Add `idempotency_key` to the chat message entity that can be used for optimistic message sending.
- Pleroma API: (`GET /api/v1/pleroma/federation_status`) Add a way to get a list of unreachable instances. - Pleroma API: (`GET /api/v1/pleroma/federation_status`) Add a way to get a list of unreachable instances.
- Mastodon API: User and conversation mutes can now auto-expire if `expires_in` parameter was given while adding the mute. - Mastodon API: User and conversation mutes can now auto-expire if `expires_in` parameter was given while adding the mute.
- Admin API: An endpoint to manage frontends - Admin API: An endpoint to manage frontends.
- Streaming API: Add follow relationships updates - Streaming API: Add follow relationships updates.
</details> </details>
### Fixed ### Fixed
- Users with `is_discoverable` field set to false (default value) will appear in in-service search results but be hidden from external services (search bots etc.). - Users with `is_discoverable` field set to false (default value) will appear in in-service search results but be hidden from external services (search bots etc.).
- Streaming API: Posts and notifications are not dropped, when CLI task is executing.
- Creating incorrect IPv4 address-style HTTP links when encountering certain numbers.
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
@ -51,24 +57,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased (Patch) ## Unreleased (Patch)
### Fixed
- Fix ability to update Pleroma Chat push notifications with PUT /api/v1/push/subscription and alert type pleroma:chat_mention
- Emoji Reaction activity filtering from blocked and muted accounts.
- StealEmojiPolicy creates dir for emojis, if it doesn't exist.
## [2.2.1] - 2020-12-22
### Changed ### Changed
- Updated Pleroma FE
### Fixed ### Fixed
- Config generation: rename `Pleroma.Upload.Filter.ExifTool` to `Pleroma.Upload.Filter.Exiftool`. - Config generation: rename `Pleroma.Upload.Filter.ExifTool` to `Pleroma.Upload.Filter.Exiftool`.
- Search: RUM index search speed has been fixed.
- S3 Uploads with Elixir 1.11. - S3 Uploads with Elixir 1.11.
- Emoji Reaction activity filtering from blocked and muted accounts.
- Mix task pleroma.user delete_activities for source installations. - Mix task pleroma.user delete_activities for source installations.
- Fix ability to update Pleroma Chat push notifications with PUT /api/v1/push/subscription and alert type pleroma:chat_mention - Search: RUM index search speed has been fixed.
- Forwarded reports duplication from Pleroma instances.
- Rich Media Previews sometimes showed the wrong preview due to a bug following redirects. - Rich Media Previews sometimes showed the wrong preview due to a bug following redirects.
- Fixes for the autolinker.
- Forwarded reports duplication from Pleroma instances.
<details> - <details>
<summary>API</summary> <summary>API</summary>
- Statuses were not displayed for Mastodon forwarded reports. - Statuses were not displayed for Mastodon forwarded reports.
</details>
</details> ### Upgrade notes
1. Restart Pleroma
## [2.2.0] - 2020-11-12 ## [2.2.0] - 2020-11-12
@ -106,7 +123,7 @@ switched to a new configuration mechanism, however it was not officially removed
- Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details). - Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details).
- Mix tasks for controlling user account confirmation status in bulk (`mix pleroma.user confirm_all` and `mix pleroma.user unconfirm_all`) - Mix tasks for controlling user account confirmation status in bulk (`mix pleroma.user confirm_all` and `mix pleroma.user unconfirm_all`)
- Mix task for sending confirmation emails to all unconfirmed users (`mix pleroma.email send_confirmation_mails`) - Mix task for sending confirmation emails to all unconfirmed users (`mix pleroma.email resend_confirmation_emails`)
- Mix task option for force-unfollowing relays - Mix task option for force-unfollowing relays
- App metrics: ability to restrict access to specified IP whitelist. - App metrics: ability to restrict access to specified IP whitelist.

View File

@ -47,7 +47,6 @@
config :pleroma, ecto_repos: [Pleroma.Repo] config :pleroma, ecto_repos: [Pleroma.Repo]
config :pleroma, Pleroma.Repo, config :pleroma, Pleroma.Repo,
types: Pleroma.PostgresTypes,
telemetry_event: [Pleroma.Repo.Instrumenter], telemetry_event: [Pleroma.Repo.Instrumenter],
migration_lock: nil migration_lock: nil
@ -64,14 +63,6 @@
filters: [Pleroma.Upload.Filter.Dedupe], filters: [Pleroma.Upload.Filter.Dedupe],
link_name: false, link_name: false,
proxy_remote: false, proxy_remote: false,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :upload
]
],
filename_display_max_length: 30, filename_display_max_length: 30,
default_description: nil default_description: nil
@ -306,7 +297,7 @@
hideSitename: false, hideSitename: false,
hideUserStats: false, hideUserStats: false,
loginMethod: "password", loginMethod: "password",
logo: "/static/logo.png", logo: "/static/logo.svg",
logoMargin: ".1em", logoMargin: ".1em",
logoMask: true, logoMask: true,
minimalScopesMode: false, minimalScopesMode: false,
@ -343,8 +334,8 @@
config :pleroma, :manifest, config :pleroma, :manifest,
icons: [ icons: [
%{ %{
src: "/static/logo.png", src: "/static/logo.svg",
type: "image/png" type: "image/svg+xml"
} }
], ],
theme_color: "#282c37", theme_color: "#282c37",
@ -648,7 +639,7 @@
} }
config :pleroma, :oauth2, config :pleroma, :oauth2,
token_expires_in: 3600 * 24 * 30, token_expires_in: 3600 * 24 * 365 * 100,
issue_new_refresh_token: true, issue_new_refresh_token: true,
clean_expired_tokens: false clean_expired_tokens: false

View File

@ -101,74 +101,10 @@
%{ %{
key: :proxy_remote, key: :proxy_remote,
type: :boolean, type: :boolean,
description: description: """
"If enabled, requests to media stored using a remote uploader will be proxied instead of being redirected" Proxy requests to the remote uploader.\n
}, Useful if media upload endpoint is not internet accessible.
%{ """
key: :proxy_opts,
label: "Proxy Options",
type: :keyword,
description: "Options for Pleroma.ReverseProxy",
suggestions: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :media
]
],
children: [
%{
key: :redirect_on_failure,
type: :boolean,
description:
"Redirects the client to the real remote URL if there's any HTTP errors. " <>
"Any error during body processing will not be redirected as the response is chunked."
},
%{
key: :max_body_length,
type: :integer,
description:
"Limits the content length to be approximately the " <>
"specified length. It is validated with the `content-length` header and also verified when proxying."
},
%{
key: :http,
label: "HTTP",
type: :keyword,
description: "HTTP options",
children: [
%{
key: :adapter,
type: :keyword,
description: "Adapter specific options",
children: [
%{
key: :ssl_options,
type: :keyword,
label: "SSL Options",
description: "SSL options for HTTP adapter",
children: [
%{
key: :versions,
type: {:list, :atom},
description: "List of TLS versions to use",
suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
}
]
}
]
},
%{
key: :proxy_url,
label: "Proxy URL",
type: [:string, :tuple],
description: "Proxy URL",
suggestions: ["127.0.0.1:8123", {:socks5, :localhost, 9050}]
}
]
}
]
}, },
%{ %{
key: :filename_display_max_length, key: :filename_display_max_length,
@ -1256,7 +1192,7 @@
hideSitename: false, hideSitename: false,
hideUserStats: false, hideUserStats: false,
loginMethod: "password", loginMethod: "password",
logo: "/static/logo.png", logo: "/static/logo.svg",
logoMargin: ".1em", logoMargin: ".1em",
logoMask: true, logoMask: true,
minimalScopesMode: false, minimalScopesMode: false,
@ -1342,7 +1278,7 @@
key: :logo, key: :logo,
type: {:string, :image}, type: {:string, :image},
description: "URL of the logo, defaults to Pleroma's logo", description: "URL of the logo, defaults to Pleroma's logo",
suggestions: ["/static/logo.png"] suggestions: ["/static/logo.svg"]
}, },
%{ %{
key: :logoMargin, key: :logoMargin,
@ -1552,7 +1488,7 @@
%{ %{
key: :enabled, key: :enabled,
type: :boolean, type: :boolean,
description: "Enables proxying of remote media to the instance's proxy" description: "Enables proxying of remote media via the instance's proxy"
}, },
%{ %{
key: :base_url, key: :base_url,
@ -1589,80 +1525,41 @@
}, },
%{ %{
key: :proxy_opts, key: :proxy_opts,
label: "Proxy Options", label: "Advanced MediaProxy Options",
type: :keyword, type: :keyword,
description: "Options for Pleroma.ReverseProxy", description: "Internal Pleroma.ReverseProxy settings",
suggestions: [ suggestions: [
redirect_on_failure: false, redirect_on_failure: false,
max_body_length: 25 * 1_048_576, max_body_length: 25 * 1_048_576,
max_read_duration: 30_000, max_read_duration: 30_000
http: [
follow_redirect: true,
pool: :media
]
], ],
children: [ children: [
%{ %{
key: :redirect_on_failure, key: :redirect_on_failure,
type: :boolean, type: :boolean,
description: description: """
"Redirects the client to the real remote URL if there's any HTTP errors. " <> Redirects the client to the origin server upon encountering HTTP errors.\n
"Any error during body processing will not be redirected as the response is chunked." Note that files larger than Max Body Length will trigger an error. (e.g., Peertube videos)\n\n
**WARNING:** This setting will allow larger files to be accessed, but exposes the\n
IP addresses of your users to the other servers, bypassing the MediaProxy.
"""
}, },
%{ %{
key: :max_body_length, key: :max_body_length,
type: :integer, type: :integer,
description: description: "Maximum file size allowed through the Pleroma MediaProxy cache."
"Limits the content length to be approximately the " <>
"specified length. It is validated with the `content-length` header and also verified when proxying."
}, },
%{ %{
key: :max_read_duration, key: :max_read_duration,
type: :integer, type: :integer,
description: "Timeout (in milliseconds) of GET request to remote URI." description: "Timeout (in milliseconds) of GET request to the remote URI."
},
%{
key: :http,
label: "HTTP",
type: :keyword,
description: "HTTP options",
children: [
%{
key: :adapter,
type: :keyword,
description: "Adapter specific options",
children: [
%{
key: :ssl_options,
type: :keyword,
label: "SSL Options",
description: "SSL options for HTTP adapter",
children: [
%{
key: :versions,
type: {:list, :atom},
description: "List of TLS version to use",
suggestions: [:tlsv1, ":tlsv1.1", ":tlsv1.2"]
}
]
}
]
},
%{
key: :proxy_url,
label: "Proxy URL",
type: [:string, :tuple],
description: "Proxy URL",
suggestions: ["127.0.0.1:8123", {:socks5, :localhost, 9050}]
}
]
} }
] ]
}, },
%{ %{
key: :whitelist, key: :whitelist,
type: {:list, :string}, type: {:list, :string},
description: "List of hosts with scheme to bypass the mediaproxy", description: "List of hosts with scheme to bypass the MediaProxy",
suggestions: ["http://example.com"] suggestions: ["http://example.com"]
} }
] ]
@ -1955,14 +1852,8 @@
group: :pleroma, group: :pleroma,
key: Oban, key: Oban,
type: :group, type: :group,
description: """ description:
[Oban](https://github.com/sorentwo/oban) asynchronous job processor configuration. "[Oban](https://github.com/sorentwo/oban) asynchronous job processor configuration.",
Note: if you are running PostgreSQL in [`silent_mode`](https://postgresqlco.nf/en/doc/param/silent_mode?version=9.1),
it's advised to set [`log_destination`](https://postgresqlco.nf/en/doc/param/log_destination?version=9.1) to `syslog`,
otherwise `postmaster.log` file may grow because of "you don't own a lock of type ShareLock" warnings
(see https://github.com/sorentwo/oban/issues/52).
""",
children: [ children: [
%{ %{
key: :log, key: :log,

View File

@ -1,2 +1,4 @@
firefox, /emoji/Firefox.gif, Gif,Fun firefox, /emoji/Firefox.gif, Gif,Fun
blank, /emoji/blank.png, Fun blank, /emoji/blank.png, Fun
dinosaur, /emoji/dino walking.gif, Gif
external_emoji, https://example.com/emoji.png

View File

@ -47,7 +47,10 @@
password: "postgres", password: "postgres",
database: "pleroma_test", database: "pleroma_test",
hostname: System.get_env("DB_HOST") || "localhost", hostname: System.get_env("DB_HOST") || "localhost",
pool: Ecto.Adapters.SQL.Sandbox pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 50
config :pleroma, :dangerzone, override_repo_pool_size: true
# Reduce hash rounds for testing # Reduce hash rounds for testing
config :pbkdf2_elixir, rounds: 1 config :pbkdf2_elixir, rounds: 1
@ -121,6 +124,20 @@
config :pleroma, :mrf, policies: [] config :pleroma, :mrf, policies: []
config :pleroma, :pipeline,
object_validator: Pleroma.Web.ActivityPub.ObjectValidatorMock,
mrf: Pleroma.Web.ActivityPub.MRFMock,
activity_pub: Pleroma.Web.ActivityPub.ActivityPubMock,
side_effects: Pleroma.Web.ActivityPub.SideEffectsMock,
federator: Pleroma.Web.FederatorMock,
config: Pleroma.ConfigMock
config :pleroma, :cachex, provider: Pleroma.CachexMock
config :pleroma, :side_effects,
ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock,
logger: Pleroma.LoggerMock
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

@ -1123,6 +1123,7 @@ Loads json generated from `config/descriptions.exs`.
```json ```json
[ [
{ {
"id": 1234,
"data": { "data": {
"actor": { "actor": {
"id": 1, "id": 1,

View File

@ -206,6 +206,7 @@ Additional parameters can be added to the JSON body/Form data:
- `pleroma_settings_store` - Opaque user settings to be saved on the backend. - `pleroma_settings_store` - Opaque user settings to be saved on the backend.
- `skip_thread_containment` - if true, skip filtering out broken threads - `skip_thread_containment` - if true, skip filtering out broken threads
- `allow_following_move` - if true, allows automatically follow moved following accounts - `allow_following_move` - if true, allows automatically follow moved following accounts
- `also_known_as` - array of ActivityPub IDs, needed for following move
- `pleroma_background_image` - sets the background image of the user. Can be set to "" (an empty string) to reset. - `pleroma_background_image` - sets the background image of the user. Can be set to "" (an empty string) to reset.
- `discoverable` - if true, external services (search bots) etc. are allowed to index / list the account (regardless of this setting, user will still appear in regular search results). - `discoverable` - if true, external services (search bots) etc. are allowed to index / list the account (regardless of this setting, user will still appear in regular search results).
- `actor_type` - the type of this account. - `actor_type` - the type of this account.

View File

@ -16,8 +16,7 @@
mix pleroma.email test [--to <destination email address>] mix pleroma.email test [--to <destination email address>]
``` ```
Example:
Example:
=== "OTP" === "OTP"
@ -36,11 +35,11 @@ Example:
=== "OTP" === "OTP"
```sh ```sh
./bin/pleroma_ctl email send_confirmation_mails ./bin/pleroma_ctl email resend_confirmation_emails
``` ```
=== "From Source" === "From Source"
```sh ```sh
mix pleroma.email send_confirmation_mails mix pleroma.email resend_confirmation_emails
``` ```

View File

@ -264,13 +264,13 @@
=== "OTP" === "OTP"
```sh ```sh
./bin/pleroma_ctl user toggle_confirmed <nickname> ./bin/pleroma_ctl user confirm <nickname>
``` ```
=== "From Source" === "From Source"
```sh ```sh
mix pleroma.user toggle_confirmed <nickname> mix pleroma.user confirm <nickname>
``` ```
## Set confirmation status for all regular active users ## Set confirmation status for all regular active users

View File

@ -9,29 +9,32 @@ static_dir="instance/static"
# project_branch="pleroma" # project_branch="pleroma"
# static_dir="priv/static" # static_dir="priv/static"
if [[ ! -d "${static_dir}" ]] if [ ! -d "${static_dir}" ]
then then
echo "Error: ${static_dir} directory is missing, are you sure you are running this script at the root of pleromas repository?" echo "Error: ${static_dir} directory is missing, are you sure you are running this script at the root of pleromas repository?"
exit 1 exit 1
fi fi
last_modified="$(curl -s -I 'https://git.pleroma.social/api/v4/projects/'${project_id}'/jobs/artifacts/'${project_branch}'/download?job=build' | grep '^Last-Modified:' | cut -d: -f2-)" last_modified="$(curl --fail -s -I 'https://git.pleroma.social/api/v4/projects/'${project_id}'/jobs/artifacts/'${project_branch}'/download?job=build' | grep '^Last-Modified:' | cut -d: -f2-)"
echo "branch:${project_branch}" echo "branch:${project_branch}"
echo "Last-Modified:${last_modified}" echo "Last-Modified:${last_modified}"
artifact="mastofe.zip" artifact="mastofe.zip"
if [[ -e mastofe.timestamp ]] && [[ "${last_modified}" != "" ]] if [ "${last_modified}x" = "x" ]
then then
if [[ "$(cat mastofe.timestamp)" == "${last_modified}" ]] echo "ERROR: Couldn't get the modification date of the latest build archive, maybe it expired, exiting..."
then exit 1
echo "MastoFE is up-to-date, exiting…"
exit 0
fi
fi fi
curl -c - "https://git.pleroma.social/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=build" -o "${artifact}" || exit if [ -e mastofe.timestamp ] && [ "$(cat mastofe.timestamp)" = "${last_modified}" ]
then
echo "MastoFE is up-to-date, exiting..."
exit 0
fi
curl --fail -c - "https://git.pleroma.social/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=build" -o "${artifact}" || exit
# TODO: Update the emoji as well # TODO: Update the emoji as well
rm -fr "${static_dir}/sw.js" "${static_dir}/packs" || exit rm -fr "${static_dir}/sw.js" "${static_dir}/packs" || exit

View File

@ -12,7 +12,8 @@ defmodule Mix.Pleroma do
:cachex, :cachex,
:flake_id, :flake_id,
:swoosh, :swoosh,
:timex :timex,
:fast_html
] ]
@cachex_children ["object", "user", "scrubber", "web_resp"] @cachex_children ["object", "user", "scrubber", "web_resp"]
@doc "Common functions to be reused in mix tasks" @doc "Common functions to be reused in mix tasks"
@ -37,12 +38,23 @@ def start_pleroma do
Enum.each(apps, &Application.ensure_all_started/1) Enum.each(apps, &Application.ensure_all_started/1)
oban_config = [
crontab: [],
repo: Pleroma.Repo,
log: false,
queues: [],
plugins: []
]
children = children =
[ [
Pleroma.Repo, Pleroma.Repo,
Pleroma.Emoji,
{Pleroma.Config.TransferTask, false}, {Pleroma.Config.TransferTask, false},
Pleroma.Web.Endpoint, Pleroma.Web.Endpoint,
{Oban, Pleroma.Config.get(Oban)} {Oban, oban_config},
{Majic.Pool,
[name: Pleroma.MajicPool, pool_size: Pleroma.Config.get([:majic_pool, :size], 2)]}
] ++ ] ++
http_children(adapter) http_children(adapter)

View File

@ -48,9 +48,15 @@ def run(["bump_all_conversations"]) do
def run(["update_users_following_followers_counts"]) do def run(["update_users_following_followers_counts"]) do
start_pleroma() start_pleroma()
User Repo.transaction(
|> Repo.all() fn ->
|> Enum.each(&User.update_follower_count/1) from(u in User, select: u)
|> Repo.stream()
|> Stream.each(&User.update_follower_count/1)
|> Stream.run()
end,
timeout: :infinity
)
end end
def run(["prune_objects" | args]) do def run(["prune_objects" | args]) do

View File

@ -161,12 +161,21 @@ def run(["gen" | rest]) do
) )
|> Path.expand() |> Path.expand()
{strip_uploads_message, strip_uploads_default} =
if Pleroma.Utils.command_available?("exiftool") do
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as installed. (y/n)",
"y"}
else
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
"n"}
end
strip_uploads = strip_uploads =
get_option( get_option(
options, options,
:strip_uploads, :strip_uploads,
"Do you want to strip location (GPS) data from uploaded images? (y/n)", strip_uploads_message,
"y" strip_uploads_default
) === "y" ) === "y"
anonymize_uploads = anonymize_uploads =
@ -253,7 +262,7 @@ def run(["gen" | rest]) do
else else
shell_error( shell_error(
"The task would have overwritten the following files:\n" <> "The task would have overwritten the following files:\n" <>
(Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <> (Enum.map(will_overwrite, &"- #{&1}\n") |> Enum.join("")) <>
"Rerun with `--force` to overwrite them." "Rerun with `--force` to overwrite them."
) )
end end

View File

@ -345,11 +345,11 @@ def run(["delete_activities", nickname]) do
end end
end end
def run(["toggle_confirmed", nickname]) do def run(["confirm", nickname]) do
start_pleroma() start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.toggle_confirmation(user) {:ok, user} = User.confirm(user)
message = if user.confirmation_pending, do: "needs", else: "doesn't need" message = if user.confirmation_pending, do: "needs", else: "doesn't need"

View File

@ -24,6 +24,8 @@ defmodule Pleroma.Activity do
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
schema "activities" do schema "activities" do
field(:data, :map) field(:data, :map)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
@ -272,7 +274,7 @@ defp get_in_reply_to_activity_from_object(%Object{data: %{"inReplyTo" => ap_id}}
defp get_in_reply_to_activity_from_object(_), do: nil defp get_in_reply_to_activity_from_object(_), do: nil
def get_in_reply_to_activity(%Activity{} = activity) do def get_in_reply_to_activity(%Activity{} = activity) do
get_in_reply_to_activity_from_object(Object.normalize(activity)) get_in_reply_to_activity_from_object(Object.normalize(activity, fetch: false))
end end
def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"]) def normalize(obj) when is_map(obj), do: get_by_ap_id_with_object(obj["id"])
@ -298,7 +300,7 @@ def delete_all_by_object_ap_id(_), do: nil
defp purge_web_resp_cache(%Activity{} = activity) do defp purge_web_resp_cache(%Activity{} = activity) do
%{path: path} = URI.parse(activity.data["id"]) %{path: path} = URI.parse(activity.data["id"])
Cachex.del(:web_resp_cache, path) @cachex.del(:web_resp_cache, path)
activity activity
end end

View File

@ -8,7 +8,7 @@ defmodule Pleroma.Activity.Ir.Topics do
def get_activity_topics(activity) do def get_activity_topics(activity) do
activity activity
|> Object.normalize() |> Object.normalize(fetch: false)
|> generate_topics(activity) |> generate_topics(activity)
|> List.flatten() |> List.flatten()
end end

19
lib/pleroma/caching.ex Normal file
View File

@ -0,0 +1,19 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Caching do
@callback get!(Cachex.cache(), any()) :: any()
@callback get(Cachex.cache(), any()) :: {atom(), any()}
@callback put(Cachex.cache(), any(), any(), Keyword.t()) :: {Cachex.status(), boolean()}
@callback put(Cachex.cache(), any(), any()) :: {Cachex.status(), boolean()}
@callback fetch!(Cachex.cache(), any(), function() | nil) :: any()
# @callback del(Cachex.cache(), any(), Keyword.t()) :: {Cachex.status(), boolean()}
@callback del(Cachex.cache(), any()) :: {Cachex.status(), boolean()}
@callback stream!(Cachex.cache(), any()) :: Enumerable.t()
@callback expire_at(Cachex.cache(), binary(), number()) :: {Cachex.status(), boolean()}
@callback exists?(Cachex.cache(), any()) :: {Cachex.status(), boolean()}
@callback execute!(Cachex.cache(), function()) :: any()
@callback get_and_update(Cachex.cache(), any(), function()) ::
{:commit | :ignore, any()}
end

View File

@ -7,6 +7,8 @@ defmodule Pleroma.Captcha do
alias Plug.Crypto.KeyGenerator alias Plug.Crypto.KeyGenerator
alias Plug.Crypto.MessageEncryptor alias Plug.Crypto.MessageEncryptor
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@doc """ @doc """
Ask the configured captcha service for a new captcha Ask the configured captcha service for a new captcha
""" """
@ -86,7 +88,7 @@ defp validate_expiration(created_at) do
end end
defp validate_usage(token) do defp validate_usage(token) do
if is_nil(Cachex.get!(:used_captcha_cache, token)) do if is_nil(@cachex.get!(:used_captcha_cache, token)) do
:ok :ok
else else
{:error, :already_used} {:error, :already_used}
@ -95,7 +97,7 @@ defp validate_usage(token) do
defp mark_captcha_as_used(token) do defp mark_captcha_as_used(token) do
ttl = seconds_valid() |> :timer.seconds() ttl = seconds_valid() |> :timer.seconds()
Cachex.put(:used_captcha_cache, token, true, ttl: ttl) @cachex.put(:used_captcha_cache, token, true, ttl: ttl)
end end
defp method, do: Pleroma.Config.get!([__MODULE__, :method]) defp method, do: Pleroma.Config.get!([__MODULE__, :method])

View File

@ -3,14 +3,18 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config do defmodule Pleroma.Config do
@behaviour Pleroma.Config.Getting
defmodule Error do defmodule Error do
defexception [:message] defexception [:message]
end end
@impl true
def get(key), do: get(key, nil) def get(key), do: get(key, nil)
@impl true
def get([key], default), do: get(key, default) def get([key], default), do: get(key, default)
@impl true
def get([_ | _] = path, default) do def get([_ | _] = path, default) do
case fetch(path) do case fetch(path) do
{:ok, value} -> value {:ok, value} -> value
@ -18,6 +22,7 @@ def get([_ | _] = path, default) do
end end
end end
@impl true
def get(key, default) do def get(key, default) do
Application.get_env(:pleroma, key, default) Application.get_env(:pleroma, key, default)
end end

View File

@ -0,0 +1,8 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Getting do
@callback get(any()) :: any()
@callback get(any(), any()) :: any()
end

View File

@ -5,6 +5,7 @@
defmodule Pleroma.Conversation do defmodule Pleroma.Conversation do
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Conversation.Participation.RecipientShip alias Pleroma.Conversation.Participation.RecipientShip
alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
use Ecto.Schema use Ecto.Schema
@ -58,7 +59,7 @@ def maybe_create_recipientships(participation, activity) do
def create_or_bump_for(activity, opts \\ []) do def create_or_bump_for(activity, opts \\ []) do
with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity), with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
"Create" <- activity.data["type"], "Create" <- activity.data["type"],
object <- Pleroma.Object.normalize(activity), %Object{} = object <- Object.normalize(activity, fetch: false),
true <- object.data["type"] in ["Note", "Question"], true <- object.data["type"] in ["Note", "Question"],
ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
{:ok, conversation} = create_for_ap_id(ap_id) {:ok, conversation} = create_for_ap_id(ap_id)

View File

@ -93,6 +93,19 @@ def account_confirmation_email(user) do
|> html_body(html_body) |> html_body(html_body)
end end
def approval_pending_email(user) do
html_body = """
<h3>Awaiting Approval</h3>
<p>Your account at #{instance_name()} is being reviewed by staff. You will receive another email once your account is approved.</p>
"""
new()
|> to(recipient(user))
|> from(sender())
|> subject("Your account is awaiting approval")
|> html_body(html_body)
end
@doc """ @doc """
Email used in digest email notifications Email used in digest email notifications
Includes Mentions and New Followers data Includes Mentions and New Followers data
@ -106,7 +119,7 @@ def digest_email(user) do
notifications notifications
|> Enum.filter(&(&1.activity.data["type"] == "Create")) |> Enum.filter(&(&1.activity.data["type"] == "Create"))
|> Enum.map(fn notification -> |> Enum.map(fn notification ->
object = Pleroma.Object.normalize(notification.activity) object = Pleroma.Object.normalize(notification.activity, fetch: false)
if not is_nil(object) do if not is_nil(object) do
object = update_in(object.data["content"], &format_links/1) object = update_in(object.data["content"], &format_links/1)
@ -129,7 +142,7 @@ def digest_email(user) do
if not is_nil(from) do if not is_nil(from) do
%{ %{
data: notification, data: notification,
object: Pleroma.Object.normalize(notification.activity), object: Pleroma.Object.normalize(notification.activity, fetch: false),
from: User.get_by_ap_id(notification.activity.actor) from: User.get_by_ap_id(notification.activity.actor)
} }
end end
@ -151,7 +164,7 @@ def digest_email(user) do
logo_path = logo_path =
if is_nil(logo) do if is_nil(logo) do
Path.join(:code.priv_dir(:pleroma), "static/static/logo.png") Path.join(:code.priv_dir(:pleroma), "static/static/logo.svg")
else else
Path.join(Config.get([:instance, :static_dir]), logo) Path.join(Config.get([:instance, :static_dir]), logo)
end end
@ -162,7 +175,7 @@ def digest_email(user) do
|> subject("Your digest from #{instance_name()}") |> subject("Your digest from #{instance_name()}")
|> put_layout(false) |> put_layout(false)
|> render_body("digest.html", html_data) |> render_body("digest.html", html_data)
|> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.png", type: :inline)) |> attachment(Swoosh.Attachment.new(logo_path, filename: "logo.svg", type: :inline))
end end
end end

View File

@ -5,6 +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.MediaProxy alias Pleroma.Web.MediaProxy
def emojify(text) do def emojify(text) do
@ -43,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, "#{Pleroma.Web.Endpoint.static_url()}#{file}") Map.put(acc, name, to_string(URI.merge(Web.base_url(), file)))
end) end)
end end

View File

@ -20,16 +20,18 @@ defmodule Pleroma.Emoji.Pack do
name: String.t() name: String.t()
} }
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
alias Pleroma.Emoji alias Pleroma.Emoji
alias Pleroma.Emoji.Pack alias Pleroma.Emoji.Pack
alias Pleroma.Utils
@spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} @spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
def create(name) do def create(name) do
with :ok <- validate_not_empty([name]), with :ok <- validate_not_empty([name]),
dir <- Path.join(emoji_path(), name), dir <- Path.join(emoji_path(), name),
:ok <- File.mkdir(dir) do :ok <- File.mkdir(dir) do
%__MODULE__{pack_file: Path.join(dir, "pack.json")} save_pack(%__MODULE__{pack_file: Path.join(dir, "pack.json")})
|> save_pack()
end end
end end
@ -62,10 +64,9 @@ def show(opts) do
@spec delete(String.t()) :: @spec delete(String.t()) ::
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values} {:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
def delete(name) do def delete(name) do
with :ok <- validate_not_empty([name]) do with :ok <- validate_not_empty([name]),
emoji_path() pack_path <- Path.join(emoji_path(), name) do
|> Path.join(name) File.rm_rf(pack_path)
|> File.rm_rf()
end end
end end
@ -94,7 +95,7 @@ defp unpack_zip_emojies(zip_files) do
def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do
with {:ok, zip_files} <- :zip.table(to_charlist(file.path)), with {:ok, zip_files} <- :zip.table(to_charlist(file.path)),
[_ | _] = emojies <- unpack_zip_emojies(zip_files), [_ | _] = emojies <- unpack_zip_emojies(zip_files),
{:ok, tmp_dir} <- Pleroma.Utils.tmp_dir("emoji") do {:ok, tmp_dir} <- Utils.tmp_dir("emoji") do
try do try do
{:ok, _emoji_files} = {:ok, _emoji_files} =
:zip.unzip( :zip.unzip(
@ -282,18 +283,21 @@ def update_metadata(name, data) do
end end
end end
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :not_found} @spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()}
def load_pack(name) do def load_pack(name) do
pack_file = Path.join([emoji_path(), name, "pack.json"]) pack_file = Path.join([emoji_path(), name, "pack.json"])
if File.exists?(pack_file) do with {:ok, _} <- File.stat(pack_file),
{:ok, pack_data} <- File.read(pack_file) do
pack = pack =
pack_file from_json(
|> File.read!() pack_data,
|> from_json() %{
|> Map.put(:pack_file, pack_file) pack_file: pack_file,
|> Map.put(:path, Path.dirname(pack_file)) path: Path.dirname(pack_file),
|> Map.put(:name, name) name: name
}
)
files_count = files_count =
pack.files pack.files
@ -301,8 +305,6 @@ def load_pack(name) do
|> length() |> length()
{:ok, Map.put(pack, :files_count, files_count)} {:ok, Map.put(pack, :files_count, files_count)}
else
{:error, :not_found}
end end
end end
@ -415,7 +417,7 @@ defp create_archive_and_cache(pack, hash) do
ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file]) ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files)) overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files))
Cachex.put!( @cachex.put(
:emoji_packs_cache, :emoji_packs_cache,
pack.name, pack.name,
# if pack.json MD5 changes, the cache is not valid anymore # if pack.json MD5 changes, the cache is not valid anymore
@ -434,10 +436,17 @@ defp save_pack(pack) do
end end
end end
defp from_json(json) do defp from_json(json, attrs) do
map = Jason.decode!(json) map = Jason.decode!(json)
struct(__MODULE__, %{files: map["files"], pack: map["pack"]}) pack_attrs =
attrs
|> Map.merge(%{
files: map["files"],
pack: map["pack"]
})
struct(__MODULE__, pack_attrs)
end end
defp validate_shareable_packs_available(uri) do defp validate_shareable_packs_available(uri) do
@ -491,10 +500,10 @@ defp rename_file(pack, filename, new_filename) do
end end
defp create_subdirs(file_path) do defp create_subdirs(file_path) do
if String.contains?(file_path, "/") do with true <- String.contains?(file_path, "/"),
file_path path <- Path.dirname(file_path),
|> Path.dirname() false <- File.exists?(path) do
|> File.mkdir_p!() File.mkdir_p!(path)
end end
end end
@ -518,10 +527,15 @@ defp remove_dir_if_empty(emoji, filename) do
defp get_filename(pack, shortcode) do defp get_filename(pack, shortcode) do
with %{^shortcode => filename} when is_binary(filename) <- pack.files, with %{^shortcode => filename} when is_binary(filename) <- pack.files,
true <- pack.path |> Path.join(filename) |> File.exists?() do file_path <- Path.join(pack.path, filename),
{:ok, _} <- File.stat(file_path) do
{:ok, filename} {:ok, filename}
else else
_ -> {:error, :doesnt_exist} {:error, _} = error ->
error
_ ->
{:error, :doesnt_exist}
end end
end end
@ -606,7 +620,7 @@ defp download_archive(url, sha) do
defp fetch_archive(pack) do defp fetch_archive(pack) do
hash = :crypto.hash(:md5, File.read!(pack.pack_file)) hash = :crypto.hash(:md5, File.read!(pack.pack_file))
case Cachex.get!(:emoji_packs_cache, pack.name) do case @cachex.get!(:emoji_packs_cache, pack.name) do
%{hash: ^hash, pack_data: archive} -> archive %{hash: ^hash, pack_data: archive} -> archive
_ -> create_archive_and_cache(pack, hash) _ -> create_archive_and_cache(pack, hash)
end end

View File

@ -76,7 +76,7 @@ def render_activities(activities) do
|> Enum.map(fn activity -> |> Enum.map(fn activity ->
user = User.get_cached_by_ap_id(activity.data["actor"]) user = User.get_cached_by_ap_id(activity.data["actor"])
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
like_count = object.data["like_count"] || 0 like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0 announcement_count = object.data["announcement_count"] || 0

View File

@ -6,6 +6,8 @@ defmodule Pleroma.HTML do
# Scrubbers are compiled on boot so they can be configured in OTP releases # Scrubbers are compiled on boot so they can be configured in OTP releases
# @on_load :compile_scrubbers # @on_load :compile_scrubbers
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def compile_scrubbers do def compile_scrubbers do
dir = Path.join(:code.priv_dir(:pleroma), "scrubbers") dir = Path.join(:code.priv_dir(:pleroma), "scrubbers")
@ -56,8 +58,8 @@ def get_cached_scrubbed_html_for_activity(
) do ) do
key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}" key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}"
Cachex.fetch!(:scrubber_cache, key, fn _key -> @cachex.fetch!(:scrubber_cache, key, fn _key ->
object = Pleroma.Object.normalize(activity) object = Pleroma.Object.normalize(activity, fetch: false)
ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback) ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback)
end) end)
end end
@ -105,7 +107,7 @@ def extract_first_external_url_from_object(%{data: %{"content" => content}} = ob
unless object.data["fake"] do unless object.data["fake"] do
key = "URL|#{object.id}" key = "URL|#{object.id}"
Cachex.fetch!(:scrubber_cache, key, fn _key -> @cachex.fetch!(:scrubber_cache, key, fn _key ->
{:commit, {:ok, extract_first_external_url(content)}} {:commit, {:ok, extract_first_external_url(content)}}
end) end)
else else

View File

@ -2,8 +2,6 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
Postgrex.Types.define( defmodule Pleroma.Logging do
Pleroma.PostgresTypes, @callback error(String.t()) :: any()
[] ++ Ecto.Adapters.Postgres.extensions(), end
json: Jason
)

View File

@ -358,7 +358,7 @@ def dismiss(%{id: user_id} = _user, id) do
def create_notifications(activity, options \\ []) def create_notifications(activity, options \\ [])
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
object = Object.normalize(activity, false) object = Object.normalize(activity, fetch: false)
if object && object.data["type"] == "Answer" do if object && object.data["type"] == "Answer" do
{:ok, []} {:ok, []}
@ -625,7 +625,7 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity,
def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false
def skip?(:filtered, activity, user) do def skip?(:filtered, activity, user) do
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
cond do cond do
is_nil(object) -> is_nil(object) ->

View File

@ -23,6 +23,8 @@ defmodule Pleroma.Object do
@derive {Jason.Encoder, only: [:data]} @derive {Jason.Encoder, only: [:data]}
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
schema "objects" do schema "objects" do
field(:data, :map) field(:data, :map)
@ -106,39 +108,42 @@ defp warn_on_no_object_preloaded(ap_id) do
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}") Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
end end
def normalize(_, fetch_remote \\ true, options \\ []) def normalize(_, options \\ [fetch: false])
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object. # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
# Use this whenever possible, especially when walking graphs in an O(N) loop! # Use this whenever possible, especially when walking graphs in an O(N) loop!
def normalize(%Object{} = object, _, _), do: object def normalize(%Object{} = object, _), do: object
def normalize(%Activity{object: %Object{} = object}, _, _), do: object def normalize(%Activity{object: %Object{} = object}, _), do: object
# A hack for fake activities # A hack for fake activities
def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _, _) do def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _) do
%Object{id: "pleroma:fake_object_id", data: data} %Object{id: "pleroma:fake_object_id", data: data}
end end
# No preloaded object # No preloaded object
def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote, _) do def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, options) do
warn_on_no_object_preloaded(ap_id) warn_on_no_object_preloaded(ap_id)
normalize(ap_id, fetch_remote) normalize(ap_id, options)
end end
# No preloaded object # No preloaded object
def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote, _) do def normalize(%Activity{data: %{"object" => ap_id}}, options) do
warn_on_no_object_preloaded(ap_id) warn_on_no_object_preloaded(ap_id)
normalize(ap_id, fetch_remote) normalize(ap_id, options)
end end
# Old way, try fetching the object through cache. # Old way, try fetching the object through cache.
def normalize(%{"id" => ap_id}, fetch_remote, _), do: normalize(ap_id, fetch_remote) def normalize(%{"id" => ap_id}, options), do: normalize(ap_id, options)
def normalize(ap_id, false, _) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
def normalize(ap_id, true, options) when is_binary(ap_id) do def normalize(ap_id, options) when is_binary(ap_id) do
Fetcher.fetch_object_from_id!(ap_id, options) if Keyword.get(options, :fetch) do
Fetcher.fetch_object_from_id!(ap_id, options)
else
get_cached_by_ap_id(ap_id)
end
end end
def normalize(_, _, _), do: nil def normalize(_, _), do: nil
# Owned objects can only be accessed by their owner # Owned objects can only be accessed by their owner
def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do
@ -156,9 +161,9 @@ def authorize_access(%Object{}, %User{}), do: :ok
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
key = "object:#{ap_id}" key = "object:#{ap_id}"
with {:ok, nil} <- Cachex.get(:object_cache, key), with {:ok, nil} <- @cachex.get(:object_cache, key),
object when not is_nil(object) <- get_by_ap_id(ap_id), object when not is_nil(object) <- get_by_ap_id(ap_id),
{:ok, true} <- Cachex.put(:object_cache, key, object) do {:ok, true} <- @cachex.put(:object_cache, key, object) do
object object
else else
{:ok, object} -> object {:ok, object} -> object
@ -216,13 +221,13 @@ def prune(%Object{data: %{"id" => _id}} = object) do
end end
def invalid_object_cache(%Object{data: %{"id" => id}}) do def invalid_object_cache(%Object{data: %{"id" => id}}) do
with {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do with {:ok, true} <- @cachex.del(:object_cache, "object:#{id}") do
Cachex.del(:web_resp_cache, URI.parse(id).path) @cachex.del(:web_resp_cache, URI.parse(id).path)
end end
end end
def set_cache(%Object{data: %{"id" => ap_id}} = object) do def set_cache(%Object{data: %{"id" => ap_id}} = object) do
Cachex.put(:object_cache, "object:#{ap_id}", object) @cachex.put(:object_cache, "object:#{ap_id}", object)
{:ok, object} {:ok, object}
end end
@ -283,7 +288,7 @@ def decrease_replies_count(ap_id) do
end end
def increase_vote_count(ap_id, name, actor) do def increase_vote_count(ap_id, name, actor) do
with %Object{} = object <- Object.normalize(ap_id), with %Object{} = object <- Object.normalize(ap_id, fetch: false),
"Question" <- object.data["type"] do "Question" <- object.data["type"] do
key = if poll_is_multiple?(object), do: "anyOf", else: "oneOf" key = if poll_is_multiple?(object), do: "anyOf", else: "oneOf"
@ -324,7 +329,7 @@ def local?(%Object{data: %{"id" => id}}) do
end end
def replies(object, opts \\ []) do def replies(object, opts \\ []) do
object = Object.normalize(object) object = Object.normalize(object, fetch: false)
query = query =
Object Object

View File

@ -83,13 +83,13 @@ def fetch_object_from_id(id, options \\ []) do
with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)}, with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])}, {_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
{_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)}, {_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
{_, nil} <- {:normalize, Object.normalize(data, false)}, {_, nil} <- {:normalize, Object.normalize(data, fetch: false)},
params <- prepare_activity_params(data), params <- prepare_activity_params(data),
{_, :ok} <- {:containment, Containment.contain_origin(id, params)}, {_, :ok} <- {:containment, Containment.contain_origin(id, params)},
{_, {:ok, activity}} <- {_, {:ok, activity}} <-
{:transmogrifier, Transmogrifier.handle_incoming(params, options)}, {:transmogrifier, Transmogrifier.handle_incoming(params, options)},
{_, _data, %Object{} = object} <- {_, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do {:object, data, Object.normalize(activity, fetch: false)} do
{:ok, object} {:ok, object}
else else
{:allowed_depth, false} -> {:allowed_depth, false} ->

View File

@ -17,6 +17,8 @@ defmodule Pleroma.ReverseProxy do
@failed_request_ttl :timer.seconds(60) @failed_request_ttl :timer.seconds(60)
@methods ~w(GET HEAD) @methods ~w(GET HEAD)
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def max_read_duration_default, do: @max_read_duration def max_read_duration_default, do: @max_read_duration
def default_cache_control_header, do: @default_cache_control_header def default_cache_control_header, do: @default_cache_control_header
@ -107,7 +109,7 @@ def call(conn = %{method: method}, url, opts) when method in @methods do
opts opts
end end
with {:ok, nil} <- Cachex.get(:failed_proxy_url_cache, url), with {:ok, nil} <- @cachex.get(:failed_proxy_url_cache, url),
{:ok, code, headers, client} <- request(method, url, req_headers, client_opts), {:ok, code, headers, client} <- request(method, url, req_headers, client_opts),
:ok <- :ok <-
header_length_constraint( header_length_constraint(
@ -427,6 +429,6 @@ defp track_failed_url(url, error, opts) do
nil nil
end end
Cachex.put(:failed_proxy_url_cache, url, true, ttl: ttl) @cachex.put(:failed_proxy_url_cache, url, true, ttl: ttl)
end end
end end

View File

@ -81,6 +81,8 @@ defmodule Pleroma.User do
] ]
] ]
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
schema "users" do schema "users" do
field(:bio, :string, default: "") field(:bio, :string, default: "")
field(:raw_bio, :string) field(:raw_bio, :string)
@ -140,7 +142,7 @@ defmodule Pleroma.User do
field(:allow_following_move, :boolean, default: true) field(:allow_following_move, :boolean, default: true)
field(:skip_thread_containment, :boolean, default: false) field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person") field(:actor_type, :string, default: "Person")
field(:also_known_as, {:array, :string}, default: []) field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
field(:inbox, :string) field(:inbox, :string)
field(:shared_inbox, :string) field(:shared_inbox, :string)
field(:accepts_chat_messages, :boolean, default: nil) field(:accepts_chat_messages, :boolean, default: nil)
@ -246,13 +248,13 @@ def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \
end end
def cached_blocked_users_ap_ids(user) do def cached_blocked_users_ap_ids(user) do
Cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ -> @cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
blocked_users_ap_ids(user) blocked_users_ap_ids(user)
end) end)
end end
def cached_muted_users_ap_ids(user) do def cached_muted_users_ap_ids(user) do
Cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ -> @cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
muted_users_ap_ids(user) muted_users_ap_ids(user)
end) end)
end end
@ -513,6 +515,7 @@ def update_changeset(struct, params \\ %{}) do
:hide_follows_count, :hide_follows_count,
:hide_favorites, :hide_favorites,
:allow_following_move, :allow_following_move,
:also_known_as,
:background, :background,
:show_role, :show_role,
:skip_thread_containment, :skip_thread_containment,
@ -521,7 +524,6 @@ def update_changeset(struct, params \\ %{}) do
:pleroma_settings_store, :pleroma_settings_store,
:is_discoverable, :is_discoverable,
:actor_type, :actor_type,
:also_known_as,
:accepts_chat_messages :accepts_chat_messages
] ]
) )
@ -806,18 +808,50 @@ def register(%Ecto.Changeset{} = changeset) do
end end
end end
def post_register_action(%User{} = user) do def post_register_action(%User{confirmation_pending: true} = user) do
with {:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
end
end
def post_register_action(%User{approval_pending: true} = user) do
with {:ok, _} <- send_user_approval_email(user),
{:ok, _} <- send_admin_approval_emails(user) do
{:ok, user}
end
end
def post_register_action(%User{approval_pending: false, confirmation_pending: false} = user) do
with {:ok, user} <- autofollow_users(user), with {:ok, user} <- autofollow_users(user),
{:ok, _} <- autofollowing_users(user), {:ok, _} <- autofollowing_users(user),
{:ok, user} <- set_cache(user), {:ok, user} <- set_cache(user),
{:ok, _} <- send_welcome_email(user), {:ok, _} <- send_welcome_email(user),
{:ok, _} <- send_welcome_message(user), {:ok, _} <- send_welcome_message(user),
{:ok, _} <- send_welcome_chat_message(user), {:ok, _} <- send_welcome_chat_message(user) do
{:ok, _} <- try_send_confirmation_email(user) do
{:ok, user} {:ok, user}
end end
end end
defp send_user_approval_email(user) do
user
|> Pleroma.Emails.UserEmail.approval_pending_email()
|> Pleroma.Emails.Mailer.deliver_async()
{:ok, :enqueued}
end
defp send_admin_approval_emails(user) do
all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end)
|> Enum.each(fn superuser ->
superuser
|> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
|> Pleroma.Emails.Mailer.deliver_async()
end)
{:ok, :enqueued}
end
def send_welcome_message(user) do def send_welcome_message(user) do
if User.WelcomeMessage.enabled?() do if User.WelcomeMessage.enabled?() do
User.WelcomeMessage.post_message(user) User.WelcomeMessage.post_message(user)
@ -1016,9 +1050,9 @@ def set_cache({:ok, user}), do: set_cache(user)
def set_cache({:error, err}), do: {:error, err} def set_cache({:error, err}), do: {:error, err}
def set_cache(%User{} = user) do def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user) @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user) @cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user)) @cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
{:ok, user} {:ok, user}
end end
@ -1041,26 +1075,26 @@ def get_user_friends_ap_ids(user) do
@spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()] @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
def get_cached_user_friends_ap_ids(user) do def get_cached_user_friends_ap_ids(user) do
Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ -> @cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
get_user_friends_ap_ids(user) get_user_friends_ap_ids(user)
end) end)
end end
def invalidate_cache(user) do def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}") @cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}") @cachex.del(:user_cache, "nickname:#{user.nickname}")
Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}") @cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
Cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}") @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
Cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}") @cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
end end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}" key = "ap_id:#{ap_id}"
with {:ok, nil} <- Cachex.get(:user_cache, key), with {:ok, nil} <- @cachex.get(:user_cache, key),
user when not is_nil(user) <- get_by_ap_id(ap_id), user when not is_nil(user) <- get_by_ap_id(ap_id),
{:ok, true} <- Cachex.put(:user_cache, key, user) do {:ok, true} <- @cachex.put(:user_cache, key, user) do
user user
else else
{:ok, user} -> user {:ok, user} -> user
@ -1072,11 +1106,11 @@ def get_cached_by_id(id) do
key = "id:#{id}" key = "id:#{id}"
ap_id = ap_id =
Cachex.fetch!(:user_cache, key, fn _ -> @cachex.fetch!(:user_cache, key, fn _ ->
user = get_by_id(id) user = get_by_id(id)
if user do if user do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user) @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
{:commit, user.ap_id} {:commit, user.ap_id}
else else
{:ignore, ""} {:ignore, ""}
@ -1089,7 +1123,7 @@ def get_cached_by_id(id) do
def get_cached_by_nickname(nickname) do def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}" key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn -> @cachex.fetch!(:user_cache, key, fn _ ->
case get_or_fetch_by_nickname(nickname) do case get_or_fetch_by_nickname(nickname) do
{:ok, user} -> {:commit, user} {:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil} {:error, _error} -> {:ignore, nil}
@ -1358,7 +1392,7 @@ def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
) )
end end
Cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}") @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
{:ok, Enum.filter([user_mute, user_notification_mute], & &1)} {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
end end
@ -1368,7 +1402,7 @@ def unmute(%User{} = muter, %User{} = mutee) do
with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee), with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
{:ok, user_notification_mute} <- {:ok, user_notification_mute} <-
UserRelationship.delete_notification_mute(muter, mutee) do UserRelationship.delete_notification_mute(muter, mutee) do
Cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}") @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
{:ok, [user_mute, user_notification_mute]} {:ok, [user_mute, user_notification_mute]}
end end
end end
@ -1590,11 +1624,34 @@ def approve(users) when is_list(users) do
end) end)
end end
def approve(%User{} = user) do def approve(%User{approval_pending: true} = user) do
change(user, approval_pending: false) with chg <- change(user, approval_pending: false),
|> update_and_set_cache() {:ok, user} <- update_and_set_cache(chg) do
post_register_action(user)
{:ok, user}
end
end end
def approve(%User{} = user), do: {:ok, user}
def confirm(users) when is_list(users) do
Repo.transaction(fn ->
Enum.map(users, fn user ->
with {:ok, user} <- confirm(user), do: user
end)
end)
end
def confirm(%User{confirmation_pending: true} = user) do
with chg <- confirmation_changeset(user, need_confirmation: false),
{:ok, user} <- update_and_set_cache(chg) do
post_register_action(user)
{:ok, user}
end
end
def confirm(%User{} = user), do: {:ok, user}
def update_notification_settings(%User{} = user, settings) do def update_notification_settings(%User{} = user, settings) do
user user
|> cast(%{notification_settings: settings}, []) |> cast(%{notification_settings: settings}, [])
@ -2081,18 +2138,6 @@ def touch_last_digest_emailed_at(user) do
updated_user updated_user
end end
@spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
def toggle_confirmation(%User{} = user) do
user
|> confirmation_changeset(need_confirmation: !user.confirmation_pending)
|> update_and_set_cache()
end
@spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
def toggle_confirmation(users) do
Enum.map(users, &toggle_confirmation/1)
end
@spec need_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()} @spec need_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
def need_confirmation(%User{} = user, bool) do def need_confirmation(%User{} = user, bool) do
user user
@ -2365,7 +2410,7 @@ def unblock_domain(user, domain_blocked) do
{:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()} {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
defp add_to_block(%User{} = user, %User{} = blocked) do defp add_to_block(%User{} = user, %User{} = blocked) do
with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
Cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}") @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
{:ok, relationship} {:ok, relationship}
end end
end end
@ -2374,7 +2419,7 @@ defp add_to_block(%User{} = user, %User{} = blocked) do
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()} {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do defp remove_from_block(%User{} = user, %User{} = blocked) do
with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
Cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}") @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
{:ok, relationship} {:ok, relationship}
end end
end end

View File

@ -3,6 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Utils do defmodule Pleroma.Utils do
@posix_error_codes ~w(
eacces eagain ebadf ebadmsg ebusy edeadlk edeadlock edquot eexist efault
efbig eftype eintr einval eio eisdir eloop emfile emlink emultihop
enametoolong enfile enobufs enodev enolck enolink enoent enomem enospc
enosr enostr enosys enotblk enotdir enotsup enxio eopnotsupp eoverflow
eperm epipe erange erofs espipe esrch estale etxtbsy exdev
)a
def compile_dir(dir) when is_binary(dir) do def compile_dir(dir) when is_binary(dir) do
dir dir
|> File.ls!() |> File.ls!()
@ -44,4 +52,12 @@ def tmp_dir(prefix \\ "") do
error -> error error -> error
end end
end end
@spec posix_error_message(atom()) :: binary()
def posix_error_message(code) when code in @posix_error_codes do
error_message = Gettext.dgettext(Pleroma.Web.Gettext, "posix_errors", "#{code}")
"(POSIX error: #{error_message})"
end
def posix_error_message(_), do: ""
end end

View File

@ -32,6 +32,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
require Logger require Logger
require Pleroma.Constants require Pleroma.Constants
@behaviour Pleroma.Web.ActivityPub.ActivityPub.Persisting
@behaviour Pleroma.Web.ActivityPub.ActivityPub.Streaming
defp get_recipients(%{"type" => "Create"} = data) do defp get_recipients(%{"type" => "Create"} = data) do
to = Map.get(data, "to", []) to = Map.get(data, "to", [])
cc = Map.get(data, "cc", []) cc = Map.get(data, "cc", [])
@ -85,13 +88,14 @@ 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]
@spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()} @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
{:ok, object, meta} {:ok, object, meta}
end end
end end
@impl true
def persist(object, meta) do def persist(object, meta) do
with local <- Keyword.fetch!(meta, :local), with local <- Keyword.fetch!(meta, :local),
{recipients, _, _} <- get_recipients(object), {recipients, _, _} <- get_recipients(object),
@ -221,6 +225,7 @@ def stream_out_participations(participations) do
Streamer.stream("participation", participations) Streamer.stream("participation", participations)
end end
@impl true
def stream_out_participations(%Object{data: %{"context" => context}}, user) do def stream_out_participations(%Object{data: %{"context" => context}}, user) do
with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do
conversation = Repo.preload(conversation, :participations) conversation = Repo.preload(conversation, :participations)
@ -237,8 +242,10 @@ def stream_out_participations(%Object{data: %{"context" => context}}, user) do
end end
end end
@impl true
def stream_out_participations(_, _), do: :noop def stream_out_participations(_, _), do: :noop
@impl true
def stream_out(%Activity{data: %{"type" => data_type}} = activity) def stream_out(%Activity{data: %{"type" => data_type}} = activity)
when data_type in ["Create", "Announce", "Delete"] do when data_type in ["Create", "Announce", "Delete"] do
activity activity
@ -246,6 +253,7 @@ def stream_out(%Activity{data: %{"type" => data_type}} = activity)
|> Streamer.stream(activity) |> Streamer.stream(activity)
end end
@impl true
def stream_out(_activity) do def stream_out(_activity) do
:noop :noop
end end
@ -600,12 +608,14 @@ def fetch_user_activities(user, reading_user, params \\ %{}) do
|> Map.put(:muting_user, reading_user) |> Map.put(:muting_user, reading_user)
end end
pagination_type = Map.get(params, :pagination_type) || :keyset
%{ %{
godmode: params[:godmode], godmode: params[:godmode],
reading_user: reading_user reading_user: reading_user
} }
|> user_activities_recipients() |> user_activities_recipients()
|> fetch_activities(params) |> fetch_activities(params, pagination_type)
|> Enum.reverse() |> Enum.reverse()
end end

View File

@ -0,0 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub.Persisting do
@callback persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
end

View File

@ -0,0 +1,12 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub.Streaming do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
@callback stream_out(Activity.t()) :: any()
@callback stream_out_participations(Object.t(), User.t()) :: any()
end

View File

@ -128,7 +128,7 @@ def activity(conn, _params) do
end end
defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
object_id = Object.normalize(activity).id object_id = Object.normalize(activity, fetch: false).id
assign(conn, :tracking_fun_data, object_id) assign(conn, :tracking_fun_data, object_id)
end end
@ -434,7 +434,7 @@ defp handle_user_activity(
end end
defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
with %Object{} = object <- Object.normalize(params["object"]), with %Object{} = object <- Object.normalize(params["object"], fetch: false),
true <- user.is_moderator || user.ap_id == object.data["actor"], true <- user.is_moderator || user.ap_id == object.data["actor"],
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]), {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
@ -445,7 +445,7 @@ defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
end end
defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
with %Object{} = object <- Object.normalize(params["object"]), with %Object{} = object <- Object.normalize(params["object"], fetch: false),
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)}, {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <- {_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline, {:common_pipeline,

View File

@ -80,7 +80,7 @@ def undo(actor, object) do
@spec delete(User.t(), String.t()) :: {:ok, map(), keyword()} @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
def delete(actor, object_id) do def delete(actor, object_id) do
object = Object.normalize(object_id, false) object = Object.normalize(object_id, fetch: false)
user = !object && User.get_cached_by_ap_id(object_id) user = !object && User.get_cached_by_ap_id(object_id)

View File

@ -5,6 +5,8 @@
defmodule Pleroma.Web.ActivityPub.MRF do defmodule Pleroma.Web.ActivityPub.MRF do
require Logger require Logger
@behaviour Pleroma.Web.ActivityPub.MRF.PipelineFiltering
@mrf_config_descriptions [ @mrf_config_descriptions [
%{ %{
group: :pleroma, group: :pleroma,
@ -72,6 +74,7 @@ def filter(policies, %{} = message) do
def filter(%{} = object), do: get_policies() |> filter(object) def filter(%{} = object), do: get_policies() |> filter(object)
@impl true
def pipeline_filter(%{} = message, meta) do def pipeline_filter(%{} = message, meta) do
object = meta[:object_data] object = meta[:object_data]
ap_id = message["object"] ap_id = message["object"]

View File

@ -31,7 +31,7 @@ def filter(%{"type" => "Create", "object" => child_object} = object)
when is_map(child_object) do when is_map(child_object) do
child = child =
child_object["inReplyTo"] child_object["inReplyTo"]
|> Object.normalize(child_object["inReplyTo"]) |> Object.normalize(fetch: false)
|> filter_by_summary(child_object) |> filter_by_summary(child_object)
object = Map.put(object, "object", child) object = Map.put(object, "object", child)

View File

@ -0,0 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.PipelineFiltering do
@callback pipeline_filter(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
end

View File

@ -10,73 +10,75 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
@moduledoc "Detect new emojis by their shortcode and steals them" @moduledoc "Detect new emojis by their shortcode and steals them"
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
defp remote_host?(host), do: host != Config.get([Pleroma.Web.Endpoint, :url, :host])
defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], []) defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
defp steal_emoji({shortcode, url}) do defp steal_emoji({shortcode, url}, emoji_dir_path) do
url = Pleroma.Web.MediaProxy.url(url) url = Pleroma.Web.MediaProxy.url(url)
{:ok, response} = Pleroma.HTTP.get(url)
size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
if byte_size(response.body) <= size_limit do with {:ok, %{status: status} = response} when status in 200..299 <- Pleroma.HTTP.get(url) do
emoji_dir_path = size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
Config.get(
[:mrf_steal_emoji, :path], if byte_size(response.body) <= size_limit do
Path.join(Config.get([:instance, :static_dir]), "emoji/stolen") extension =
url
|> URI.parse()
|> Map.get(:path)
|> Path.basename()
|> Path.extname()
file_path = Path.join(emoji_dir_path, shortcode <> (extension || ".png"))
case File.write(file_path, response.body) do
:ok ->
shortcode
e ->
Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
nil
end
else
Logger.debug(
"MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{
size_limit
} B)"
) )
extension = nil
url
|> URI.parse()
|> Map.get(:path)
|> Path.basename()
|> Path.extname()
file_path = Path.join([emoji_dir_path, shortcode <> (extension || ".png")])
try do
:ok = File.write(file_path, response.body)
shortcode
rescue
e ->
Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
nil
end end
else else
Logger.debug( e ->
"MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{ Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
size_limit nil
} B)"
)
nil
end end
rescue
e ->
Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
nil
end end
@impl true @impl true
def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = message) do def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = message) do
host = URI.parse(actor).host host = URI.parse(actor).host
if remote_host?(host) and accept_host?(host) do if host != Pleroma.Web.Endpoint.host() and accept_host?(host) do
installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end) installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
emoji_dir_path =
Config.get(
[:mrf_steal_emoji, :path],
Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
)
File.mkdir_p(emoji_dir_path)
new_emojis = new_emojis =
foreign_emojis foreign_emojis
|> Enum.filter(fn {shortcode, _url} -> shortcode not in installed_emoji end) |> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end)
|> Enum.filter(fn {shortcode, _url} -> |> Enum.filter(fn {shortcode, _url} ->
reject_emoji? = reject_emoji? =
Config.get([:mrf_steal_emoji, :rejected_shortcodes], []) [:mrf_steal_emoji, :rejected_shortcodes]
|> Config.get([])
|> Enum.find(false, fn regex -> String.match?(shortcode, regex) end) |> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)
!reject_emoji? !reject_emoji?
end) end)
|> Enum.map(&steal_emoji(&1)) |> Enum.map(&steal_emoji(&1, emoji_dir_path))
|> Enum.filter(& &1) |> Enum.filter(& &1)
if !Enum.empty?(new_emojis) do if !Enum.empty?(new_emojis) do

View File

@ -9,6 +9,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
the system. the system.
""" """
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object alias Pleroma.Object
@ -32,7 +34,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} @impl true
def validate(object, meta) def validate(object, meta)
def validate(%{"type" => type} = object, meta) def validate(%{"type" => type} = object, meta)
@ -286,7 +288,7 @@ def fetch_actor(object) do
def fetch_actor_and_object(object) do def fetch_actor_and_object(object) do
fetch_actor(object) fetch_actor(object)
Object.normalize(object["object"], true) Object.normalize(object["object"], fetch: true)
:ok :ok
end end
end end

View File

@ -0,0 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidator.Validating do
@callback validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
end

View File

@ -14,12 +14,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)
@federator Config.get([:pipeline, :federator], Federator)
@object_validator Config.get([:pipeline, :object_validator], ObjectValidator)
@mrf Config.get([:pipeline, :mrf], MRF)
@activity_pub Config.get([:pipeline, :activity_pub], ActivityPub)
@config 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) do
{:ok, {:ok, activity, meta}} -> {:ok, {:ok, activity, meta}} ->
SideEffects.handle_after_transaction(meta) @side_effects.handle_after_transaction(meta)
{:ok, activity, meta} {:ok, activity, meta}
{:ok, value} -> {:ok, value} ->
@ -35,13 +42,13 @@ def common_pipeline(object, meta) do
def do_common_pipeline(object, meta) do def do_common_pipeline(object, meta) do
with {_, {:ok, validated_object, meta}} <- with {_, {:ok, validated_object, meta}} <-
{:validate_object, ObjectValidator.validate(object, meta)}, {:validate_object, @object_validator.validate(object, meta)},
{_, {:ok, mrfd_object, meta}} <- {_, {:ok, mrfd_object, meta}} <-
{:mrf_object, MRF.pipeline_filter(validated_object, meta)}, {:mrf_object, @mrf.pipeline_filter(validated_object, meta)},
{_, {:ok, activity, meta}} <- {_, {:ok, activity, meta}} <-
{:persist_object, ActivityPub.persist(mrfd_object, meta)}, {:persist_object, @activity_pub.persist(mrfd_object, meta)},
{_, {:ok, activity, meta}} <- {_, {:ok, activity, meta}} <-
{:execute_side_effects, SideEffects.handle(activity, meta)}, {:execute_side_effects, @side_effects.handle(activity, meta)},
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do {_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta} {:ok, activity, meta}
else else
@ -54,7 +61,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 =
@ -64,7 +71,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

@ -130,7 +130,7 @@ defp recipients(actor, activity) do
fetchers = fetchers =
with %Activity{data: %{"type" => "Delete"}} <- activity, with %Activity{data: %{"type" => "Delete"}} <- activity,
%Object{id: object_id} <- Object.normalize(activity), %Object{id: object_id} <- Object.normalize(activity, fetch: false),
fetchers <- User.get_delivered_users_by_object_id(object_id), fetchers <- User.get_delivered_users_by_object_id(object_id),
_ <- Delivery.delete_all_by_object_id(object_id) do _ <- Delivery.delete_all_by_object_id(object_id) do
fetchers fetchers

View File

@ -27,11 +27,19 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
require Logger require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@ap_streamer Pleroma.Config.get([:side_effects, :ap_streamer], ActivityPub)
@logger Pleroma.Config.get([:side_effects, :logger], Logger)
@behaviour Pleroma.Web.ActivityPub.SideEffects.Handling
@impl true
def handle(object, meta \\ []) def handle(object, meta \\ [])
# Task this handles # Task this handles
# - Follows # - Follows
# - Sends a notification # - Sends a notification
@impl true
def handle( def handle(
%{ %{
data: %{ data: %{
@ -59,6 +67,7 @@ def handle(
# - Rejects all existing follow activities for this person # - Rejects all existing follow activities for this person
# - Updates the follow state # - Updates the follow state
# - Dismisses notification # - Dismisses notification
@impl true
def handle( def handle(
%{ %{
data: %{ data: %{
@ -85,6 +94,7 @@ def handle(
# - Follows if possible # - Follows if possible
# - Sends a notification # - Sends a notification
# - Generates accept or reject if appropriate # - Generates accept or reject if appropriate
@impl true
def handle( def handle(
%{ %{
data: %{ data: %{
@ -126,6 +136,7 @@ def handle(
# Tasks this handles: # Tasks this handles:
# - Unfollow and block # - Unfollow and block
@impl true
def handle( def handle(
%{data: %{"type" => "Block", "object" => blocked_user, "actor" => blocking_user}} = %{data: %{"type" => "Block", "object" => blocked_user, "actor" => blocking_user}} =
object, object,
@ -144,6 +155,7 @@ def handle(
# #
# For a local user, we also get a changeset with the full information, so we # For a local user, we also get a changeset with the full information, so we
# can update non-federating, non-activitypub settings as well. # can update non-federating, non-activitypub settings as well.
@impl true
def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do
if changeset = Keyword.get(meta, :user_update_changeset) do if changeset = Keyword.get(meta, :user_update_changeset) do
changeset changeset
@ -162,6 +174,7 @@ def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object,
# Tasks this handles: # Tasks this handles:
# - Add like to object # - Add like to object
# - Set up notification # - Set up notification
@impl true
def handle(%{data: %{"type" => "Like"}} = object, meta) do def handle(%{data: %{"type" => "Like"}} = object, meta) do
liked_object = Object.get_by_ap_id(object.data["object"]) liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object) Utils.add_like_to_object(object, liked_object)
@ -179,6 +192,7 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do
# - Increase replies count # - Increase replies count
# - Set up ActivityExpiration # - Set up ActivityExpiration
# - Set up notifications # - Set up notifications
@impl true
def handle(%{data: %{"type" => "Create"}} = activity, meta) do def handle(%{data: %{"type" => "Create"}} = activity, meta) do
with {:ok, object, meta} <- handle_object_creation(meta[:object_data], meta), with {:ok, object, meta} <- handle_object_creation(meta[:object_data], meta),
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
@ -207,6 +221,7 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
# - Add announce to object # - Add announce to object
# - Set up notification # - Set up notification
# - Stream out the announce # - Stream out the announce
@impl true
def handle(%{data: %{"type" => "Announce"}} = object, meta) do def handle(%{data: %{"type" => "Announce"}} = object, meta) do
announced_object = Object.get_by_ap_id(object.data["object"]) announced_object = Object.get_by_ap_id(object.data["object"])
user = User.get_cached_by_ap_id(object.data["actor"]) user = User.get_cached_by_ap_id(object.data["actor"])
@ -224,6 +239,7 @@ def handle(%{data: %{"type" => "Announce"}} = object, meta) do
{:ok, object, meta} {:ok, object, meta}
end end
@impl true
def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
with undone_object <- Activity.get_by_ap_id(undone_object), with undone_object <- Activity.get_by_ap_id(undone_object),
:ok <- handle_undoing(undone_object) do :ok <- handle_undoing(undone_object) do
@ -234,6 +250,7 @@ def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, met
# Tasks this handles: # Tasks this handles:
# - Add reaction to object # - Add reaction to object
# - Set up notification # - Set up notification
@impl true
def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
reacted_object = Object.get_by_ap_id(object.data["object"]) reacted_object = Object.get_by_ap_id(object.data["object"])
Utils.add_emoji_reaction_to_object(object, reacted_object) Utils.add_emoji_reaction_to_object(object, reacted_object)
@ -250,9 +267,10 @@ def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
# - Reduce the user note count # - Reduce the user note count
# - Reduce the reply count # - Reduce the reply count
# - Stream out the activity # - Stream out the activity
@impl true
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
deleted_object = deleted_object =
Object.normalize(deleted_object, false) || Object.normalize(deleted_object, fetch: false) ||
User.get_cached_by_ap_id(deleted_object) User.get_cached_by_ap_id(deleted_object)
result = result =
@ -271,12 +289,12 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
MessageReference.delete_for_object(deleted_object) MessageReference.delete_for_object(deleted_object)
ActivityPub.stream_out(object) @ap_streamer.stream_out(object)
ActivityPub.stream_out_participations(deleted_object, user) @ap_streamer.stream_out_participations(deleted_object, user)
:ok :ok
else else
{:actor, _} -> {:actor, _} ->
Logger.error("The object doesn't have an actor: #{inspect(deleted_object)}") @logger.error("The object doesn't have an actor: #{inspect(deleted_object)}")
:no_object_actor :no_object_actor
end end
@ -295,6 +313,7 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
end end
# Nothing to do # Nothing to do
@impl true
def handle(object, meta) do def handle(object, meta) do
{:ok, object, meta} {:ok, object, meta}
end end
@ -312,7 +331,7 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do
{:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id) {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
{:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id) {:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id)
Cachex.put( @cachex.put(
:chat_message_id_idempotency_key_cache, :chat_message_id_idempotency_key_cache,
cm_ref.id, cm_ref.id,
meta[:idempotency_key] meta[:idempotency_key]
@ -439,6 +458,7 @@ defp add_notifications(meta, notifications) do
|> Keyword.put(:notifications, notifications ++ existing) |> Keyword.put(:notifications, notifications ++ existing)
end end
@impl true
def handle_after_transaction(meta) do def handle_after_transaction(meta) do
meta meta
|> send_notifications() |> send_notifications()

View File

@ -0,0 +1,8 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.SideEffects.Handling do
@callback handle(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
@callback handle_after_transaction(map()) :: map()
end

View File

@ -653,7 +653,9 @@ def handle_incoming(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do def get_obj_helper(id, options \\ []) do
case Object.normalize(id, true, options) do options = Keyword.put(options, :fetch, true)
case Object.normalize(id, options) do
%Object{} = object -> {:ok, object} %Object{} = object -> {:ok, object}
_ -> nil _ -> nil
end end
@ -672,7 +674,7 @@ def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id
"actor" => attributed_to, "actor" => attributed_to,
"object" => data "object" => data
}) do }) do
{:ok, Object.normalize(activity)} {:ok, Object.normalize(activity, fetch: false)}
else else
_ -> get_obj_helper(object_id) _ -> get_obj_helper(object_id)
end end
@ -763,7 +765,7 @@ def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
when activity_type in ["Create", "Listen"] do when activity_type in ["Create", "Listen"] do
object = object =
object_id object_id
|> Object.normalize() |> Object.normalize(fetch: false)
|> Map.get(:data) |> Map.get(:data)
|> prepare_object |> prepare_object
@ -779,7 +781,7 @@ def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
object = object =
object_id object_id
|> Object.normalize() |> Object.normalize(fetch: false)
data = data =
if Visibility.is_private?(object) && object.data["actor"] == ap_id do if Visibility.is_private?(object) && object.data["actor"] == ap_id do
@ -919,7 +921,7 @@ def add_emoji_tags(object), do: object
defp build_emoji_tag({name, url}) do defp build_emoji_tag({name, url}) do
%{ %{
"icon" => %{"url" => url, "type" => "Image"}, "icon" => %{"url" => "#{URI.encode(url)}", "type" => "Image"},
"name" => ":" <> name <> ":", "name" => ":" <> name <> ":",
"type" => "Emoji", "type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z", "updated" => "1970-01-01T00:00:00Z",

View File

@ -18,7 +18,7 @@ def render("object.json", %{object: %Object{} = object}) do
def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity}) def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity})
when activity_type in ["Create", "Listen"] do when activity_type in ["Create", "Listen"] do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header() base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
additional = additional =
Transmogrifier.prepare_object(activity.data) Transmogrifier.prepare_object(activity.data)
@ -29,7 +29,7 @@ def render("object.json", %{object: %Activity{data: %{"type" => activity_type}}
def render("object.json", %{object: %Activity{} = activity}) do def render("object.json", %{object: %Activity{} = activity}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header() base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
additional = additional =
Transmogrifier.prepare_object(activity.data) Transmogrifier.prepare_object(activity.data)

View File

@ -112,7 +112,8 @@ def render("user.json", %{user: user}) do
"tag" => emoji_tags, "tag" => emoji_tags,
# Note: key name is indeed "discoverable" (not an error) # Note: key name is indeed "discoverable" (not an error)
"discoverable" => user.is_discoverable, "discoverable" => user.is_discoverable,
"capabilities" => capabilities "capabilities" => capabilities,
"alsoKnownAs" => user.also_known_as
} }
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))

View File

@ -103,13 +103,15 @@ def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickna
godmode = params["godmode"] == "true" || params["godmode"] == true godmode = params["godmode"] == "true" || params["godmode"] == true
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
{_, page_size} = page_params(params) {page, page_size} = page_params(params)
activities = activities =
ActivityPub.fetch_user_activities(user, nil, %{ ActivityPub.fetch_user_activities(user, nil, %{
limit: page_size, limit: page_size,
offset: (page - 1) * page_size,
godmode: godmode, godmode: godmode,
exclude_reblogs: not with_reblogs exclude_reblogs: not with_reblogs,
pagination_type: :offset
}) })
conn conn
@ -415,7 +417,7 @@ def reload_emoji(conn, _params) do
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1) users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
User.toggle_confirmation(users) User.confirm(users)
ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"}) ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"})

View File

@ -9,6 +9,8 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug( plug(
@ -38,7 +40,7 @@ def index(%{assigns: %{user: _}} = conn, params) do
defp fetch_entries(params) do defp fetch_entries(params) do
MediaProxy.cache_table() MediaProxy.cache_table()
|> Cachex.stream!(Cachex.Query.create(true, :key)) |> @cachex.stream!(Cachex.Query.create(true, :key))
|> filter_entries(params[:query]) |> filter_entries(params[:query])
end end

View File

@ -69,6 +69,7 @@ def render("show.json", %{user: user}) do
%{ %{
"id" => user.id, "id" => user.id,
"email" => user.email,
"avatar" => avatar, "avatar" => avatar,
"nickname" => user.nickname, "nickname" => user.nickname,
"display_name" => display_name, "display_name" => display_name,

View File

@ -21,6 +21,7 @@ def render("show.json", %{log_entry: log_entry}) do
|> DateTime.to_unix() |> DateTime.to_unix()
%{ %{
id: log_entry.id,
data: log_entry.data, data: log_entry.data,
time: time, time: time,
message: ModerationLog.get_log_entry_message(log_entry) message: ModerationLog.get_log_entry_message(log_entry)

View File

@ -19,8 +19,7 @@ def render("index.json", %{reports: reports}) do
reports: reports:
reports[:items] reports[:items]
|> Enum.map(&Report.extract_report_info/1) |> Enum.map(&Report.extract_report_info/1)
|> Enum.map(&render(__MODULE__, "show.json", &1)) |> Enum.map(&render(__MODULE__, "show.json", &1)),
|> Enum.reverse(),
total: reports[:total] total: reports[:total]
} }
end end

View File

@ -614,6 +614,12 @@ defp update_credentials_request do
nullable: true, nullable: true,
description: "Allows automatically follow moved following accounts" description: "Allows automatically follow moved following accounts"
}, },
also_known_as: %Schema{
type: :array,
items: %Schema{type: :string},
nullable: true,
description: "List of alternate ActivityPub IDs"
},
pleroma_background_image: %Schema{ pleroma_background_image: %Schema{
type: :string, type: :string,
nullable: true, nullable: true,
@ -644,6 +650,7 @@ defp update_credentials_request do
pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}}, pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}},
skip_thread_containment: false, skip_thread_containment: false,
allow_following_move: false, allow_following_move: false,
also_known_as: ["https://foo.bar/users/foo"],
discoverable: false, discoverable: false,
actor_type: "Person" actor_type: "Person"
} }

View File

@ -27,7 +27,8 @@ def create_operation do
422 => Operation.response("Unprocessable Entity", "application/json", ApiError), 422 => Operation.response("Unprocessable Entity", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError), 404 => Operation.response("Not Found", "application/json", ApiError),
400 => Operation.response("Bad Request", "application/json", ApiError), 400 => Operation.response("Bad Request", "application/json", ApiError),
409 => Operation.response("Conflict", "application/json", ApiError) 409 => Operation.response("Conflict", "application/json", ApiError),
500 => Operation.response("Error", "application/json", ApiError)
} }
} }
end end

View File

@ -169,7 +169,8 @@ def delete_operation do
responses: %{ responses: %{
200 => ok_response(), 200 => ok_response(),
400 => Operation.response("Bad Request", "application/json", ApiError), 400 => Operation.response("Bad Request", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError) 404 => Operation.response("Not Found", "application/json", ApiError),
500 => Operation.response("Error", "application/json", ApiError)
} }
} }
end end
@ -184,7 +185,8 @@ def update_operation do
parameters: [name_param()], parameters: [name_param()],
responses: %{ responses: %{
200 => Operation.response("Metadata", "application/json", metadata()), 200 => Operation.response("Metadata", "application/json", metadata()),
400 => Operation.response("Bad Request", "application/json", ApiError) 400 => Operation.response("Bad Request", "application/json", ApiError),
500 => Operation.response("Error", "application/json", ApiError)
} }
} }
end end

View File

@ -40,6 +40,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
pleroma: %Schema{ pleroma: %Schema{
type: :object, type: :object,
properties: %{ properties: %{
ap_id: %Schema{type: :string},
also_known_as: %Schema{type: :array, items: %Schema{type: :string}},
allow_following_move: %Schema{ allow_following_move: %Schema{
type: :boolean, type: :boolean,
description: "whether the user allows automatically follow moved following accounts" description: "whether the user allows automatically follow moved following accounts"

View File

@ -142,7 +142,7 @@ def delete(activity_id, user) do
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <- with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(activity_id)}, {:find_activity, Activity.get_by_id(activity_id)},
{_, %Object{} = object, _} <- {_, %Object{} = object, _} <-
{:find_object, Object.normalize(activity, false), activity}, {:find_object, Object.normalize(activity, fetch: false), activity},
true <- User.superuser?(user) || user.ap_id == object.data["actor"], true <- User.superuser?(user) || user.ap_id == object.data["actor"],
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]), {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
@ -173,7 +173,7 @@ def delete(activity_id, user) do
def repeat(id, user, params \\ %{}) do def repeat(id, user, params \\ %{}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
object = %Object{} <- Object.normalize(activity, false), object = %Object{} <- Object.normalize(activity, fetch: false),
{_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)}, {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
public = public_announce?(object, params), public = public_announce?(object, params),
{:ok, announce, _} <- Builder.announce(user, object, public: public), {:ok, announce, _} <- Builder.announce(user, object, public: public),
@ -191,7 +191,7 @@ def repeat(id, user, params \\ %{}) do
def unrepeat(id, user) do def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <- with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)}, {:find_activity, Activity.get_by_id(id)},
%Object{} = note <- Object.normalize(activity, false), %Object{} = note <- Object.normalize(activity, fetch: false),
%Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note), %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
{:ok, undo, _} <- Builder.undo(user, announce), {:ok, undo, _} <- Builder.undo(user, announce),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
@ -253,7 +253,7 @@ def favorite_helper(user, id) do
def unfavorite(id, user) do def unfavorite(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <- with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)}, {:find_activity, Activity.get_by_id(id)},
%Object{} = note <- Object.normalize(activity, false), %Object{} = note <- Object.normalize(activity, fetch: false),
%Activity{} = like <- Utils.get_existing_like(user.ap_id, note), %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
{:ok, undo, _} <- Builder.undo(user, like), {:ok, undo, _} <- Builder.undo(user, like),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do {:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
@ -266,7 +266,7 @@ def unfavorite(id, user) do
def react_with_emoji(id, user, emoji) do def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id), with %Activity{} = activity <- Activity.get_by_id(id),
object <- Object.normalize(activity), object <- Object.normalize(activity, fetch: false),
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji), {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
{:ok, activity} {:ok, activity}
@ -377,7 +377,7 @@ def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(
def get_replied_to_visibility(nil), do: nil def get_replied_to_visibility(nil), do: nil
def get_replied_to_visibility(activity) do def get_replied_to_visibility(activity) do
with %Object{} = object <- Object.normalize(activity) do with %Object{} = object <- Object.normalize(activity, fetch: false) do
Visibility.get_visibility(object) Visibility.get_visibility(object)
end end
end end

View File

@ -319,7 +319,7 @@ def make_note_data(%ActivityDraft{} = draft) do
defp add_in_reply_to(object, nil), do: object defp add_in_reply_to(object, nil), do: object
defp add_in_reply_to(object, in_reply_to) do defp add_in_reply_to(object, in_reply_to) do
with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to, fetch: false) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
else else
_ -> object _ -> object
@ -399,7 +399,7 @@ def maybe_notify_mentioned_recipients(
%Activity{data: %{"to" => _to, "type" => type} = data} = activity %Activity{data: %{"to" => _to, "type" => type} = data} = activity
) )
when type == "Create" do when type == "Create" do
object = Object.normalize(activity, false) object = Object.normalize(activity, fetch: false)
object_data = object_data =
cond do cond do

View File

@ -31,7 +31,7 @@ def show(conn, %{"id" => id}) do
end end
defp get_counts(%Activity{} = activity) do defp get_counts(%Activity{} = activity) do
%Object{data: data} = Object.normalize(activity) %Object{data: data} = Object.normalize(activity, fetch: false)
%{ %{
likes: Map.get(data, "like_count", 0), likes: Map.get(data, "like_count", 0),

View File

@ -15,6 +15,8 @@ defmodule Pleroma.Web.Federator do
require Logger require Logger
@behaviour Pleroma.Web.Federator.Publishing
@doc """ @doc """
Returns `true` if the distance to target object does not exceed max configured value. Returns `true` if the distance to target object does not exceed max configured value.
Serves to prevent fetching of very long threads, especially useful on smaller instances. Serves to prevent fetching of very long threads, especially useful on smaller instances.
@ -39,10 +41,12 @@ def incoming_ap_doc(params) do
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params}) ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params})
end end
@impl true
def publish(%{id: "pleroma:fakeid"} = activity) do def publish(%{id: "pleroma:fakeid"} = activity) do
perform(:publish, activity) perform(:publish, activity)
end end
@impl true
def publish(activity) do def publish(activity) do
PublisherWorker.enqueue("publish", %{"activity_id" => activity.id}) PublisherWorker.enqueue("publish", %{"activity_id" => activity.id})
end end

View File

@ -0,0 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Federator.Publishing do
@callback publish(map()) :: any()
end

View File

@ -23,7 +23,7 @@ def pub_date(date) when is_binary(date) do
def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}") def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}")
def prepare_activity(activity, opts \\ []) do def prepare_activity(activity, opts \\ []) do
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
actor = actor =
if opts[:actor] do if opts[:actor] do
@ -51,7 +51,7 @@ def most_recent_update(activities, user) do
def feed_logo do def feed_logo do
case Pleroma.Config.get([:feed, :logo]) do case Pleroma.Config.get([:feed, :logo]) do
nil -> nil ->
"#{Pleroma.Web.base_url()}/static/logo.png" "#{Pleroma.Web.base_url()}/static/logo.svg"
logo -> logo ->
"#{Pleroma.Web.base_url()}#{logo}" "#{Pleroma.Web.base_url()}#{logo}"

View File

@ -184,6 +184,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
:show_role, :show_role,
:skip_thread_containment, :skip_thread_containment,
:allow_following_move, :allow_following_move,
:also_known_as,
:accepts_chat_messages :accepts_chat_messages
] ]
|> Enum.reduce(%{}, fn key, acc -> |> Enum.reduce(%{}, fn key, acc ->
@ -207,6 +208,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
if bot, do: {:ok, "Service"}, else: {:ok, "Person"} if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
end) end)
|> Maps.put_if_present(:actor_type, params[:actor_type]) |> Maps.put_if_present(:actor_type, params[:actor_type])
|> Maps.put_if_present(:also_known_as, params[:also_known_as])
# Note: param name is indeed :locked (not an error) # Note: param name is indeed :locked (not an error)
|> Maps.put_if_present(:is_locked, params[:locked]) |> Maps.put_if_present(:is_locked, params[:locked])
# Note: param name is indeed :discoverable (not an error) # Note: param name is indeed :discoverable (not an error)

View File

@ -26,6 +26,8 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PollOperation
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@doc "GET /api/v1/polls/:id" @doc "GET /api/v1/polls/:id"
def show(%{assigns: %{user: user}} = conn, %{id: id}) do def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60), with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
@ -55,7 +57,7 @@ def vote(%{assigns: %{user: user}, body_params: %{choices: choices}} = conn, %{i
defp get_cached_vote_or_vote(user, object, choices) do defp get_cached_vote_or_vote(user, object, choices) do
idempotency_key = "polls:#{user.id}:#{object.data["id"]}" idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
Cachex.fetch!(:idempotency_cache, idempotency_key, fn -> @cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
case CommonAPI.vote(user, object, choices) do case CommonAPI.vote(user, object, choices) do
{:error, _message} = res -> {:ignore, res} {:error, _message} = res -> {:ignore, res}
res -> {:commit, res} res -> {:commit, res}

View File

@ -318,7 +318,7 @@ def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
with true <- Pleroma.Config.get([:instance, :show_reactions]), with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(id), %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)}, {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"likes" => likes}} <- Object.normalize(activity) do %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
users = users =
User User
|> Ecto.Query.where([u], u.ap_id in ^likes) |> Ecto.Query.where([u], u.ap_id in ^likes)
@ -339,7 +339,7 @@ def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id), with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)}, {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"announcements" => announces, "id" => ap_id}} <- %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
Object.normalize(activity) do Object.normalize(activity, fetch: false) do
announces = announces =
"Announce" "Announce"
|> Activity.Queries.by_type() |> Activity.Queries.by_type()

View File

@ -187,18 +187,14 @@ defp do_render("show.json", %{user: user} = opts) do
header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true) header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true)
following_count = following_count =
if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do if !user.hide_follows_count or !user.hide_follows or opts[:for] == user,
user.following_count || 0 do: user.following_count,
else else: 0
0
end
followers_count = followers_count =
if !user.hide_followers_count or !user.hide_followers or opts[:for] == user do if !user.hide_followers_count or !user.hide_followers or opts[:for] == user,
user.follower_count || 0 do: user.follower_count,
else else: 0
0
end
bot = user.actor_type == "Service" bot = user.actor_type == "Service"
@ -269,6 +265,7 @@ defp do_render("show.json", %{user: user} = opts) do
# Pleroma extension # Pleroma extension
pleroma: %{ pleroma: %{
ap_id: user.ap_id, ap_id: user.ap_id,
also_known_as: user.also_known_as,
confirmation_pending: user.confirmation_pending, confirmation_pending: user.confirmation_pending,
tags: user.tags, tags: user.tags,
hide_followers_count: user.hide_followers_count, hide_followers_count: user.hide_followers_count,

View File

@ -139,7 +139,7 @@ defp put_emoji(response, activity) do
end end
defp put_chat_message(response, activity, reading_user, opts) do defp put_chat_message(response, activity, reading_user, opts) do
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
author = User.get_cached_by_ap_id(object.data["actor"]) author = User.get_cached_by_ap_id(object.data["actor"])
chat = Pleroma.Chat.get(reading_user.id, author.ap_id) chat = Pleroma.Chat.get(reading_user.id, author.ap_id)
cm_ref = MessageReference.for_chat_and_object(chat, object) cm_ref = MessageReference.for_chat_and_object(chat, object)

View File

@ -41,7 +41,7 @@ defp get_replied_to_activities(activities) do
activities activities
|> Enum.map(fn |> Enum.map(fn
%{data: %{"type" => "Create"}} = activity -> %{data: %{"type" => "Create"}} = activity ->
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
object && object.data["inReplyTo"] != "" && object.data["inReplyTo"] object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
_ -> _ ->
@ -51,7 +51,7 @@ defp get_replied_to_activities(activities) do
|> Activity.create_by_object_ap_id_with_object() |> Activity.create_by_object_ap_id_with_object()
|> Repo.all() |> Repo.all()
|> Enum.reduce(%{}, fn activity, acc -> |> Enum.reduce(%{}, fn activity, acc ->
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
if object, do: Map.put(acc, object.data["id"], activity), else: acc if object, do: Map.put(acc, object.data["id"], activity), else: acc
end) end)
end end
@ -65,7 +65,7 @@ defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
defp get_context_id(_), do: nil defp get_context_id(_), do: nil
defp reblogged?(activity, user) do defp reblogged?(activity, user) do
object = Object.normalize(activity) || %{} object = Object.normalize(activity, fetch: false) || %{}
present?(user && user.ap_id in (object.data["announcements"] || [])) present?(user && user.ap_id in (object.data["announcements"] || []))
end end
@ -84,7 +84,7 @@ def render("index.json", opts) do
parent_activities = parent_activities =
activities activities
|> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"])) |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
|> Enum.map(&Object.normalize(&1).data["id"]) |> Enum.map(&Object.normalize(&1, fetch: false).data["id"])
|> Activity.create_by_object_ap_id() |> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left) |> Activity.with_preloaded_object(:left)
|> Activity.with_preloaded_bookmark(reading_user) |> Activity.with_preloaded_bookmark(reading_user)
@ -124,7 +124,7 @@ def render(
) do ) do
user = CommonAPI.get_user(activity.data["actor"]) user = CommonAPI.get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"]) created_at = Utils.to_masto_date(activity.data["published"])
activity_object = Object.normalize(activity) activity_object = Object.normalize(activity, fetch: false)
reblogged_parent_activity = reblogged_parent_activity =
if opts[:parent_activities] do if opts[:parent_activities] do
@ -193,7 +193,7 @@ def render(
end end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
user = CommonAPI.get_user(activity.data["actor"]) user = CommonAPI.get_user(activity.data["actor"])
user_follower_address = user.follower_address user_follower_address = user.follower_address
@ -451,7 +451,7 @@ def render("context.json", %{activity: activity, activities: activities, user: u
end end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
with nil <- replied_to_activities[object.data["inReplyTo"]] do with nil <- replied_to_activities[object.data["inReplyTo"]] do
# If user didn't participate in the thread # If user didn't participate in the thread
@ -460,7 +460,7 @@ def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
end end
def get_reply_to(%{data: %{"object" => _object}} = activity, _) do def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
Activity.get_create_by_object_ap_id(object.data["inReplyTo"]) Activity.get_create_by_object_ap_id(object.data["inReplyTo"])

View File

@ -12,29 +12,31 @@ defmodule Pleroma.Web.MediaProxy do
@base64_opts [padding: false] @base64_opts [padding: false]
@cache_table :banned_urls_cache @cache_table :banned_urls_cache
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def cache_table, do: @cache_table def cache_table, do: @cache_table
@spec in_banned_urls(String.t()) :: boolean() @spec in_banned_urls(String.t()) :: boolean()
def in_banned_urls(url), do: elem(Cachex.exists?(@cache_table, url(url)), 1) def in_banned_urls(url), do: elem(@cachex.exists?(@cache_table, url(url)), 1)
def remove_from_banned_urls(urls) when is_list(urls) do def remove_from_banned_urls(urls) when is_list(urls) do
Cachex.execute!(@cache_table, fn cache -> @cachex.execute!(@cache_table, fn cache ->
Enum.each(Invalidation.prepare_urls(urls), &Cachex.del(cache, &1)) Enum.each(Invalidation.prepare_urls(urls), &@cachex.del(cache, &1))
end) end)
end end
def remove_from_banned_urls(url) when is_binary(url) do def remove_from_banned_urls(url) when is_binary(url) do
Cachex.del(@cache_table, url(url)) @cachex.del(@cache_table, url(url))
end end
def put_in_banned_urls(urls) when is_list(urls) do def put_in_banned_urls(urls) when is_list(urls) do
Cachex.execute!(@cache_table, fn cache -> @cachex.execute!(@cache_table, fn cache ->
Enum.each(Invalidation.prepare_urls(urls), &Cachex.put(cache, &1, true)) Enum.each(Invalidation.prepare_urls(urls), &@cachex.put(cache, &1, true))
end) end)
end end
def put_in_banned_urls(url) when is_binary(url) do def put_in_banned_urls(url) when is_binary(url) do
Cachex.put(@cache_table, url(url), true) @cachex.put(@cache_table, url(url), true)
end end
def url(url) when is_nil(url) or url == "", do: nil def url(url) when is_nil(url) or url == "", do: nil

View File

@ -74,14 +74,14 @@ def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
cond do cond do
format in ["json", "activity+json"] -> format in ["json", "activity+json"] ->
if activity.local do if activity.local do
%{data: %{"id" => redirect_url}} = Object.normalize(activity) %{data: %{"id" => redirect_url}} = Object.normalize(activity, fetch: false)
redirect(conn, external: redirect_url) redirect(conn, external: redirect_url)
else else
{:error, :not_found} {:error, :not_found}
end end
activity.data["type"] == "Create" -> activity.data["type"] == "Create" ->
%Object{} = object = Object.normalize(activity) %Object{} = object = Object.normalize(activity, fetch: false)
RedirectController.redirector_with_meta( RedirectController.redirector_with_meta(
conn, conn,
@ -112,7 +112,7 @@ def notice_player(conn, %{"id" => id}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id), with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.is_public?(activity), true <- Visibility.is_public?(activity),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)}, {_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
%Object{} = object <- Object.normalize(activity), %Object{} = object <- Object.normalize(activity, fetch: false),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
conn conn

View File

@ -82,7 +82,7 @@ def post_chat_message(
media_id: params[:media_id], media_id: params[:media_id],
idempotency_key: idempotency_key(conn) idempotency_key: idempotency_key(conn)
), ),
message <- Object.normalize(activity, false), message <- Object.normalize(activity, fetch: false),
cm_ref <- MessageReference.for_chat_and_object(chat, message) do cm_ref <- MessageReference.for_chat_and_object(chat, message) do
conn conn
|> put_view(MessageReferenceView) |> put_view(MessageReferenceView)

View File

@ -42,7 +42,10 @@ def create(%{body_params: params} = conn, %{name: pack_name}) do
|> json(%{error: "pack name, shortcode or filename cannot be empty"}) |> json(%{error: "pack name, shortcode or filename cannot be empty"})
{:error, _} = error -> {:error, _} = error ->
handle_error(conn, error, %{pack_name: pack_name}) handle_error(conn, error, %{
pack_name: pack_name,
message: "Unexpected error occurred while adding file to pack."
})
end end
end end
@ -69,7 +72,11 @@ def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack
|> json(%{error: "new_shortcode or new_filename cannot be empty"}) |> json(%{error: "new_shortcode or new_filename cannot be empty"})
{:error, _} = error -> {:error, _} = error ->
handle_error(conn, error, %{pack_name: pack_name, code: shortcode}) handle_error(conn, error, %{
pack_name: pack_name,
code: shortcode,
message: "Unexpected error occurred while updating."
})
end end
end end
@ -84,7 +91,11 @@ def delete(conn, %{name: pack_name, shortcode: shortcode}) do
|> json(%{error: "pack name or shortcode cannot be empty"}) |> json(%{error: "pack name or shortcode cannot be empty"})
{:error, _} = error -> {:error, _} = error ->
handle_error(conn, error, %{pack_name: pack_name, code: shortcode}) handle_error(conn, error, %{
pack_name: pack_name,
code: shortcode,
message: "Unexpected error occurred while deleting emoji file."
})
end end
end end
@ -94,18 +105,24 @@ defp handle_error(conn, {:error, :doesnt_exist}, %{code: emoji_code}) do
|> json(%{error: "Emoji \"#{emoji_code}\" does not exist"}) |> json(%{error: "Emoji \"#{emoji_code}\" does not exist"})
end end
defp handle_error(conn, {:error, :not_found}, %{pack_name: pack_name}) do defp handle_error(conn, {:error, :enoent}, %{pack_name: pack_name}) do
conn conn
|> put_status(:not_found) |> put_status(:not_found)
|> json(%{error: "pack \"#{pack_name}\" is not found"}) |> json(%{error: "pack \"#{pack_name}\" is not found"})
end end
defp handle_error(conn, {:error, _}, _) do defp handle_error(conn, {:error, error}, opts) do
render_error( message =
conn, [
:internal_server_error, Map.get(opts, :message, "Unexpected error occurred."),
"Unexpected error occurred while adding file to pack." Pleroma.Utils.posix_error_message(error)
) ]
|> Enum.join(" ")
|> String.trim()
conn
|> put_status(:internal_server_error)
|> json(%{error: message})
end end
defp get_filename(%Plug.Upload{filename: filename}), do: filename defp get_filename(%Plug.Upload{filename: filename}), do: filename

View File

@ -71,7 +71,7 @@ def show(conn, %{name: name, page: page, page_size: page_size}) do
with {:ok, pack} <- Pack.show(name: name, page: page, page_size: page_size) do with {:ok, pack} <- Pack.show(name: name, page: page, page_size: page_size) do
json(conn, pack) json(conn, pack)
else else
{:error, :not_found} -> {:error, :enoent} ->
conn conn
|> put_status(:not_found) |> put_status(:not_found)
|> json(%{error: "Pack #{name} does not exist"}) |> json(%{error: "Pack #{name} does not exist"})
@ -80,6 +80,17 @@ def show(conn, %{name: name, page: page, page_size: page_size}) do
conn conn
|> put_status(:bad_request) |> put_status(:bad_request)
|> json(%{error: "pack name cannot be empty"}) |> json(%{error: "pack name cannot be empty"})
{:error, error} ->
error_message =
add_posix_error(
"Failed to get the contents of the `#{name}` pack.",
error
)
conn
|> put_status(:internal_server_error)
|> json(%{error: error_message})
end end
end end
@ -95,7 +106,7 @@ def archive(conn, %{name: name}) do
"Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
}) })
{:error, :not_found} -> {:error, :enoent} ->
conn conn
|> put_status(:not_found) |> put_status(:not_found)
|> json(%{error: "Pack #{name} does not exist"}) |> json(%{error: "Pack #{name} does not exist"})
@ -116,10 +127,10 @@ def download(%{body_params: %{url: url, name: name} = params} = conn, _) do
|> put_status(:internal_server_error) |> put_status(:internal_server_error)
|> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"}) |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
{:error, e} -> {:error, error} ->
conn conn
|> put_status(:internal_server_error) |> put_status(:internal_server_error)
|> json(%{error: e}) |> json(%{error: error})
end end
end end
@ -139,12 +150,16 @@ def create(conn, %{name: name}) do
|> put_status(:bad_request) |> put_status(:bad_request)
|> json(%{error: "pack name cannot be empty"}) |> json(%{error: "pack name cannot be empty"})
{:error, _} -> {:error, error} ->
render_error( error_message =
conn, add_posix_error(
:internal_server_error, "Unexpected error occurred while creating pack.",
"Unexpected error occurred while creating pack." error
) )
conn
|> put_status(:internal_server_error)
|> json(%{error: error_message})
end end
end end
@ -164,10 +179,12 @@ def delete(conn, %{name: name}) do
|> put_status(:bad_request) |> put_status(:bad_request)
|> json(%{error: "pack name cannot be empty"}) |> json(%{error: "pack name cannot be empty"})
{:error, _, _} -> {:error, error, _} ->
error_message = add_posix_error("Couldn't delete the `#{name}` pack", error)
conn conn
|> put_status(:internal_server_error) |> put_status(:internal_server_error)
|> json(%{error: "Couldn't delete the pack #{name}"}) |> json(%{error: error_message})
end end
end end
@ -180,12 +197,16 @@ def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do
|> put_status(:bad_request) |> put_status(:bad_request)
|> json(%{error: "The fallback archive does not have all files specified in pack.json"}) |> json(%{error: "The fallback archive does not have all files specified in pack.json"})
{:error, _} -> {:error, error} ->
render_error( error_message =
conn, add_posix_error(
:internal_server_error, "Unexpected error occurred while updating pack metadata.",
"Unexpected error occurred while updating pack metadata." error
) )
conn
|> put_status(:internal_server_error)
|> json(%{error: error_message})
end end
end end
@ -204,4 +225,10 @@ def import_from_filesystem(conn, _params) do
|> json(%{error: "Error accessing emoji pack directory"}) |> json(%{error: "Error accessing emoji pack directory"})
end end
end end
defp add_posix_error(msg, error) do
[msg, Pleroma.Utils.posix_error_message(error)]
|> Enum.join(" ")
|> String.trim()
end
end end

View File

@ -29,7 +29,7 @@ def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
with true <- Pleroma.Config.get([:instance, :show_reactions]), with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(activity_id), %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
%Object{data: %{"reactions" => reactions}} when is_list(reactions) <- %Object{data: %{"reactions" => reactions}} when is_list(reactions) <-
Object.normalize(activity) do Object.normalize(activity, fetch: false) do
reactions = reactions =
reactions reactions
|> filter(params) |> filter(params)

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Web.PleromaAPI.BackupView do
def render("show.json", %{backup: %Backup{} = backup}) do def render("show.json", %{backup: %Backup{} = backup}) do
%{ %{
id: backup.id,
content_type: backup.content_type, content_type: backup.content_type,
url: download_url(backup), url: download_url(backup),
file_size: backup.file_size, file_size: backup.file_size,

View File

@ -10,6 +10,8 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def render( def render(
"show.json", "show.json",
%{ %{
@ -51,7 +53,7 @@ def render("index.json", opts) do
end end
defp put_idempotency_key(data) do defp put_idempotency_key(data) do
with {:ok, idempotency_key} <- Cachex.get(:chat_message_id_idempotency_key_cache, data.id) do with {:ok, idempotency_key} <- @cachex.get(:chat_message_id_idempotency_key_cache, data.id) do
data data
|> Maps.put_if_present(:idempotency_key, idempotency_key) |> Maps.put_if_present(:idempotency_key, idempotency_key)
else else

View File

@ -15,7 +15,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
object = Object.normalize(activity) object = Object.normalize(activity, fetch: false)
user = CommonAPI.get_user(activity.data["actor"]) user = CommonAPI.get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"]) created_at = Utils.to_masto_date(activity.data["published"])

View File

@ -41,6 +41,8 @@ def index(conn, _params) do
@defaults %{ttl: nil, query_params: true} @defaults %{ttl: nil, query_params: true}
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@impl true @impl true
def init([]), do: @defaults def init([]), do: @defaults
@ -53,7 +55,7 @@ def init(opts) do
def call(%{method: "GET"} = conn, opts) do def call(%{method: "GET"} = conn, opts) do
key = cache_key(conn, opts) key = cache_key(conn, opts)
case Cachex.get(:web_resp_cache, key) do case @cachex.get(:web_resp_cache, key) do
{:ok, nil} -> {:ok, nil} ->
cache_resp(conn, opts) cache_resp(conn, opts)
@ -97,11 +99,11 @@ defp cache_resp(conn, opts) do
conn = conn =
unless opts[:tracking_fun] do unless opts[:tracking_fun] do
Cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl) @cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl)
conn conn
else else
tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil) tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil)
Cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl) @cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl)
opts.tracking_fun.(conn, tracking_fun_data) opts.tracking_fun.(conn, tracking_fun_data)
end end

View File

@ -8,6 +8,8 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlug do
@behaviour Plug @behaviour Plug
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@impl true @impl true
def init(opts), do: opts def init(opts), do: opts
@ -25,7 +27,7 @@ def call(%{method: method} = conn, _) when method in ["POST", "PUT", "PATCH"] do
def call(conn, _), do: conn def call(conn, _), do: conn
def process_request(conn, key) do def process_request(conn, key) do
case Cachex.get(:idempotency_cache, key) do case @cachex.get(:idempotency_cache, key) do
{:ok, nil} -> {:ok, nil} ->
cache_resposnse(conn, key) cache_resposnse(conn, key)
@ -43,7 +45,7 @@ defp cache_resposnse(conn, key) do
content_type = get_content_type(conn) content_type = get_content_type(conn)
record = {request_id, content_type, conn.status, conn.resp_body} record = {request_id, content_type, conn.status, conn.resp_body}
{:ok, _} = Cachex.put(:idempotency_cache, key, record) {:ok, _} = @cachex.put(:idempotency_cache, key, record)
conn conn
|> put_resp_header("idempotency-key", key) |> put_resp_header("idempotency-key", key)

View File

@ -72,6 +72,8 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
require Logger require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@doc false @doc false
def init(plug_opts) do def init(plug_opts) do
plug_opts plug_opts
@ -124,7 +126,7 @@ def inspect_bucket(conn, bucket_name_root, plug_opts) do
key_name = make_key_name(action_settings) key_name = make_key_name(action_settings)
limit = get_limits(action_settings) limit = get_limits(action_settings)
case Cachex.get(bucket_name, key_name) do case @cachex.get(bucket_name, key_name) do
{:error, :no_cache} -> {:error, :no_cache} ->
@inspect_bucket_not_found @inspect_bucket_not_found
@ -157,7 +159,7 @@ defp check_rate(action_settings) do
key_name = make_key_name(action_settings) key_name = make_key_name(action_settings)
limit = get_limits(action_settings) limit = get_limits(action_settings)
case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do case @cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
{:commit, value} -> {:commit, value} ->
{:ok, value} {:ok, value}

View File

@ -87,8 +87,15 @@ defp get_media(conn, {:static_dir, directory}, _, opts) do
end end
defp get_media(conn, {:url, url}, true, _) do defp get_media(conn, {:url, url}, true, _) do
proxy_opts = [
http: [
follow_redirect: true,
pool: :upload
]
]
conn conn
|> Pleroma.ReverseProxy.call(url, Pleroma.Config.get([Pleroma.Upload, :proxy_opts], [])) |> Pleroma.ReverseProxy.call(url, proxy_opts)
end end
defp get_media(conn, {:url, url}, _, _) do defp get_media(conn, {:url, url}, _, _) do

View File

@ -32,7 +32,7 @@ def perform(
mastodon_type = notification.type mastodon_type = notification.type
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key) gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor) avatar_url = User.avatar_url(actor)
object = Object.normalize(activity, false) object = Object.normalize(activity, fetch: false)
user = User.get_cached_by_id(user_id) user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user) direct_conversation_id = Activity.direct_conversation_id(activity, user)

View File

@ -12,8 +12,9 @@ defmodule Pleroma.Web.RelMe do
if Pleroma.Config.get(:env) == :test do if Pleroma.Config.get(:env) == :test do
def parse(url) when is_binary(url), do: parse_url(url) def parse(url) when is_binary(url), do: parse_url(url)
else else
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def parse(url) when is_binary(url) do def parse(url) when is_binary(url) do
Cachex.fetch!(:rel_me_cache, url, fn _ -> @cachex.fetch!(:rel_me_cache, url, fn _ ->
{:commit, parse_url(url)} {:commit, parse_url(url)}
end) end)
rescue rescue

View File

@ -69,7 +69,7 @@ def fetch_data_for_object(object) do
def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do
with true <- Config.get([:rich_media, :enabled]), with true <- Config.get([:rich_media, :enabled]),
%Object{} = object <- Object.normalize(activity) do %Object{} = object <- Object.normalize(activity, fetch: false) do
fetch_data_for_object(object) fetch_data_for_object(object)
else else
_ -> %{} _ -> %{}

View File

@ -5,6 +5,8 @@
defmodule Pleroma.Web.RichMedia.Parser do defmodule Pleroma.Web.RichMedia.Parser do
require Logger require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
defp parsers do defp parsers do
Pleroma.Config.get([:rich_media, :parsers]) Pleroma.Config.get([:rich_media, :parsers])
end end
@ -24,7 +26,7 @@ def parse(url) do
end end
defp get_cached_or_parse(url) do defp get_cached_or_parse(url) do
case Cachex.fetch(:rich_media_cache, url, fn -> case @cachex.fetch(:rich_media_cache, url, fn ->
case parse_url(url) do case parse_url(url) do
{:ok, _} = res -> {:ok, _} = res ->
{:commit, res} {:commit, res}
@ -64,7 +66,7 @@ defp set_error_ttl(_url, {:content_type, _}), do: :ok
defp set_error_ttl(url, _reason) do defp set_error_ttl(url, _reason) do
ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000) ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000)
Cachex.expire(:rich_media_cache, url, ttl) @cachex.expire(:rich_media_cache, url, ttl)
:ok :ok
end end
@ -106,7 +108,7 @@ def set_ttl_based_on_image(data, url) do
{:ok, ttl} when is_number(ttl) -> {:ok, ttl} when is_number(ttl) ->
ttl = ttl * 1000 ttl = ttl * 1000
case Cachex.expire_at(:rich_media_cache, url, ttl) do case @cachex.expire_at(:rich_media_cache, url, ttl) do
{:ok, true} -> {:ok, ttl} {:ok, true} -> {:ok, ttl}
{:ok, false} -> {:error, :no_key} {:ok, false} -> {:error, :no_key}
end end

View File

@ -122,7 +122,7 @@ defp not_found(conn, message) do
end end
defp get_counts(%Activity{} = activity) do defp get_counts(%Activity{} = activity) do
%Object{data: data} = Object.normalize(activity) %Object{data: data} = Object.normalize(activity, fetch: false)
%{ %{
likes: data["like_count"] || 0, likes: data["like_count"] || 0,

View File

@ -151,7 +151,7 @@ def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do
recipients = MapSet.new(item.recipients) recipients = MapSet.new(item.recipients)
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
with parent <- Object.normalize(item) || item, with parent <- Object.normalize(item, fetch: false) || item,
true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)), true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids, true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
true <- true <-

View File

@ -126,7 +126,7 @@
<div align="center" class="img-container center" <div align="center" class="img-container center"
style="padding-right: 0px;padding-left: 0px;"> style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="center"><![endif]--><img <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="center"><![endif]--><img
align="center" alt="Image" border="0" class="center" src="cid:logo.png" align="center" alt="Image" border="0" class="center" src="cid:logo.svg"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: 80px; width: auto; max-height: 80px; display: block;" style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: 80px; width: auto; max-height: 80px; display: block;"
title="Image" height="80" /> title="Image" height="80" />
<!--[if mso]></td></tr></table><![endif]--> <!--[if mso]></td></tr></table><![endif]-->

View File

@ -31,10 +31,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- User.get_cached_by_id(uid), with %User{} = user <- User.get_cached_by_id(uid),
true <- user.local and user.confirmation_pending and user.confirmation_token == token, true <- user.local and user.confirmation_pending and user.confirmation_token == token,
{:ok, _} <- {:ok, _} <- User.confirm(user) do
user
|> User.confirmation_changeset(need_confirmation: false)
|> User.update_and_set_cache() do
redirect(conn, to: "/") redirect(conn, to: "/")
end end
end end

View File

@ -45,7 +45,6 @@ defp create_user(params, opts) do
case User.register(changeset) do case User.register(changeset) do
{:ok, user} -> {:ok, user} ->
maybe_notify_admins(user)
{:ok, user} {:ok, user}
{:error, changeset} -> {:error, changeset} ->
@ -58,18 +57,6 @@ defp create_user(params, opts) do
end end
end end
defp maybe_notify_admins(%User{} = account) do
if Pleroma.Config.get([:instance, :account_approval_required]) do
User.all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end)
|> Enum.each(fn superuser ->
superuser
|> Pleroma.Emails.AdminEmail.new_unapproved_registration(account)
|> Pleroma.Emails.Mailer.deliver_async()
end)
end
end
def password_reset(nickname_or_email) do def password_reset(nickname_or_email) do
with true <- is_binary(nickname_or_email), with true <- is_binary(nickname_or_email),
%User{local: true, email: email, deactivated: false} = user when is_binary(email) <- %User{local: true, email: email, deactivated: false} = user when is_binary(email) <-

View File

@ -58,12 +58,16 @@ defp gather_links(%User{} = user) do
] ++ Publisher.gather_webfinger_links(user) ] ++ Publisher.gather_webfinger_links(user)
end end
defp gather_aliases(%User{} = user) do
[user.ap_id | user.also_known_as]
end
def represent_user(user, "JSON") do def represent_user(user, "JSON") do
{:ok, user} = User.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
%{ %{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
"aliases" => [user.ap_id], "aliases" => gather_aliases(user),
"links" => gather_links(user) "links" => gather_links(user)
} }
end end
@ -71,6 +75,11 @@ def represent_user(user, "JSON") do
def represent_user(user, "XML") do def represent_user(user, "XML") do
{:ok, user} = User.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)
aliases =
user
|> gather_aliases()
|> Enum.map(&{:Alias, &1})
links = links =
gather_links(user) gather_links(user)
|> Enum.map(fn link -> {:Link, link} end) |> Enum.map(fn link -> {:Link, link} end)
@ -79,9 +88,8 @@ def represent_user(user, "XML") do
:XRD, :XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[ [
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"}, {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"}
{:Alias, user.ap_id} ] ++ aliases ++ links
] ++ links
} }
|> XmlBuilder.to_doc() |> XmlBuilder.to_doc()
end end
@ -116,6 +124,9 @@ defp webfinger_from_json(doc) do
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} -> {"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
Map.put(data, "ap_id", link["href"]) Map.put(data, "ap_id", link["href"])
{nil, "http://ostatus.org/schema/1.0/subscribe"} ->
Map.put(data, "subscribe_address", link["template"])
_ -> _ ->
Logger.debug("Unhandled type: #{inspect(link["type"])}") Logger.debug("Unhandled type: #{inspect(link["type"])}")
data data

10
mix.exs
View File

@ -22,7 +22,7 @@ def project do
docs: [ docs: [
source_url_pattern: source_url_pattern:
"https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}", "https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}",
logo: "priv/static/static/logo.png", logo: "priv/static/images/logo.png",
extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/**/*.md"), extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/**/*.md"),
groups_for_extras: [ groups_for_extras: [
"Installation manuals": Path.wildcard("docs/installation/*.md"), "Installation manuals": Path.wildcard("docs/installation/*.md"),
@ -147,8 +147,8 @@ defp deps do
{:earmark, "1.4.3"}, {:earmark, "1.4.3"},
{:bbcode_pleroma, "~> 0.2.0"}, {:bbcode_pleroma, "~> 0.2.0"},
{:crypt, {:crypt,
git: "https://github.com/msantos/crypt.git", git: "https://git.pleroma.social/pleroma/elixir-libraries/crypt.git",
ref: "f63a705f92c26955977ee62a313012e309a4d77a"}, ref: "cf2aa3f11632e8b0634810a15b3e612c7526f6a3"},
{:cors_plug, "~> 2.0"}, {:cors_plug, "~> 2.0"},
{:web_push_encryption, "~> 0.3"}, {:web_push_encryption, "~> 0.3"},
{:swoosh, "~> 1.0"}, {:swoosh, "~> 1.0"},
@ -158,7 +158,7 @@ defp deps do
{:floki, "~> 0.27"}, {:floki, "~> 0.27"},
{:timex, "~> 3.6"}, {:timex, "~> 3.6"},
{:ueberauth, "~> 0.4"}, {:ueberauth, "~> 0.4"},
{:linkify, "~> 0.4.0"}, {:linkify, "~> 0.4.1"},
{:http_signatures, "~> 0.1.0"}, {:http_signatures, "~> 0.1.0"},
{:telemetry, "~> 0.3"}, {:telemetry, "~> 0.3"},
{:poolboy, "~> 1.5"}, {:poolboy, "~> 1.5"},
@ -211,7 +211,7 @@ defp deps do
git: "https://git.pleroma.social/pleroma/elixir-libraries/hackney.git", git: "https://git.pleroma.social/pleroma/elixir-libraries/hackney.git",
ref: "7d7119f0651515d6d7669c78393fd90950a3ec6e", ref: "7d7119f0651515d6d7669c78393fd90950a3ec6e",
override: true}, override: true},
{:mox, "~> 0.5", only: :test}, {:mox, "~> 1.0", only: :test},
{:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test} {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test}
] ++ oauth_deps() ] ++ oauth_deps()
end end

View File

@ -22,7 +22,7 @@
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
"credo": {:hex, :credo, "1.4.1", "16392f1edd2cdb1de9fe4004f5ab0ae612c92e230433968eab00aafd976282fc", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "155f8a2989ad77504de5d8291fa0d41320fdcaa6a1030472e9967f285f8c7692"}, "credo": {:hex, :credo, "1.4.1", "16392f1edd2cdb1de9fe4004f5ab0ae612c92e230433968eab00aafd976282fc", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "155f8a2989ad77504de5d8291fa0d41320fdcaa6a1030472e9967f285f8c7692"},
"crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"crypt": {:git, "https://github.com/msantos/crypt.git", "f63a705f92c26955977ee62a313012e309a4d77a", [ref: "f63a705f92c26955977ee62a313012e309a4d77a"]}, "crypt": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/crypt.git", "cf2aa3f11632e8b0634810a15b3e612c7526f6a3", [ref: "cf2aa3f11632e8b0634810a15b3e612c7526f6a3"]},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
@ -33,7 +33,7 @@
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
"ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"}, "ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"},
"eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"}, "eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"},
"elixir_make": {:hex, :elixir_make, "0.6.1", "8faa29a5597faba999aeeb72bbb9c91694ef8068f0131192fb199f98d32994ef", [:mix], [], "hexpm", "35d33270680f8d839a4003c3e9f43afb595310a592405a00afc12de4c7f55a18"}, "elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
"esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"}, "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"},
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"},
"ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
@ -65,7 +65,7 @@
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
"libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"},
"linkify": {:hex, :linkify, "0.4.0", "7845b6ac33050a41acaf9318923ce6e7f3854418be9a5f22184de103f7a68ff9", [:mix], [], "hexpm", "a0ceb4c78591fecccf1d99fecc10c13dba75a307c663c80e28af9e2cdd9776ee"}, "linkify": {:hex, :linkify, "0.4.1", "f881eb3429ae88010cf736e6fb3eed406c187bcdd544902ec937496636b7c7b3", [:mix], [], "hexpm", "ce98693f54ae9ace59f2f7a8aed3de2ef311381a8ce7794804bd75484c371dda"},
"majic": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/majic.git", "4c692e544b28d1f5e543fb8a44be090f8cd96f80", [ref: "4c692e544b28d1f5e543fb8a44be090f8cd96f80"]}, "majic": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/majic.git", "4c692e544b28d1f5e543fb8a44be090f8cd96f80", [ref: "4c692e544b28d1f5e543fb8a44be090f8cd96f80"]},
"makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
@ -76,7 +76,7 @@
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "6fae404799408300f863550392635d8f7e3da6b71abdd5c393faf41b131c8728"}, "mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "6fae404799408300f863550392635d8f7e3da6b71abdd5c393faf41b131c8728"},
"mogrify": {:hex, :mogrify, "0.7.4", "9b2496dde44b1ce12676f85d7dc531900939e6367bc537c7243a1b089435b32d", [:mix], [], "hexpm", "50d79e337fba6bc95bfbef918058c90f50b17eed9537771e61d4619488f099c3"}, "mogrify": {:hex, :mogrify, "0.7.4", "9b2496dde44b1ce12676f85d7dc531900939e6367bc537c7243a1b089435b32d", [:mix], [], "hexpm", "50d79e337fba6bc95bfbef918058c90f50b17eed9537771e61d4619488f099c3"},
"mox": {:hex, :mox, "0.5.2", "55a0a5ba9ccc671518d068c8dddd20eeb436909ea79d1799e2209df7eaa98b6c", [:mix], [], "hexpm", "df4310628cd628ee181df93f50ddfd07be3e5ecc30232d3b6aadf30bdfe6092b"}, "mox": {:hex, :mox, "1.0.0", "4b3c7005173f47ff30641ba044eb0fe67287743eec9bd9545e37f3002b0a9f8b", [:mix], [], "hexpm", "201b0a20b7abdaaab083e9cf97884950f8a30a1350a1da403b3145e213c6f4df"},
"myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]}, "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
"nimble_pool": {:hex, :nimble_pool, "0.1.0", "ffa9d5be27eee2b00b0c634eb649aa27f97b39186fec3c493716c2a33e784ec6", [:mix], [], "hexpm", "343a1eaa620ddcf3430a83f39f2af499fe2370390d4f785cd475b4df5acaf3f9"}, "nimble_pool": {:hex, :nimble_pool, "0.1.0", "ffa9d5be27eee2b00b0c634eb649aa27f97b39186fec3c493716c2a33e784ec6", [:mix], [], "hexpm", "343a1eaa620ddcf3430a83f39f2af499fe2370390d4f785cd475b4df5acaf3f9"},

View File

@ -0,0 +1,141 @@
## This file is a PO Template file.
msgid "eperm"
msgstr "Operation not permitted"
msgid "eacces"
msgstr "Permission denied"
msgid "eagain"
msgstr "Resource temporarily unavailable"
msgid "ebadf"
msgstr "Bad file descriptor"
msgid "ebadmsg"
msgstr "Bad message"
msgid "ebusy"
msgstr "Device or resource busy"
msgid "edeadlk"
msgstr "Resource deadlock avoided"
msgid "edeadlock"
msgstr "Resource deadlock avoided"
msgid "edquot"
msgstr "Disk quota exceeded"
msgid "eexist"
msgstr "File exists"
msgid "efault"
msgstr "Bad address"
msgid "efbig"
msgstr "File is too large"
msgid "eftype"
msgstr "Inappropriate file type or format"
msgid "eintr"
msgstr "Interrupted system call"
msgid "einval"
msgstr "Invalid argument"
msgid "eio"
msgstr "Input/output error"
msgid "eisdir"
msgstr "Illegal operation on a directory"
msgid "eloop"
msgstr "Too many levels of symbolic links"
msgid "emfile"
msgstr "Too many open files"
msgid "emlink"
msgstr "Too many links"
msgid "emultihop"
msgstr "Multihop attempted"
msgid "enametoolong"
msgstr "File name is too long"
msgid "enfile"
msgstr "Too many open files in system"
msgid "enobufs"
msgstr "No buffer space available"
msgid "enodev"
msgstr "No such device"
msgid "enolck"
msgstr "No locks available"
msgid "enolink"
msgstr "Link has been severed"
msgid "enoent"
msgstr "No such file or directory"
msgid "enomem"
msgstr "Cannot allocate memory"
msgid "enospc"
msgstr "No space left on device"
msgid "enosr"
msgstr "Out of streams resources"
msgid "enostr"
msgstr "Device is not a stream"
msgid "enosys"
msgstr "Function not implemented"
msgid "enotblk"
msgstr "Block device required"
msgid "enotdir"
msgstr "Not a directory"
msgid "enotsup"
msgstr "Operation not supported"
msgid "enxio"
msgstr "No such device or address"
msgid "eopnotsupp"
msgstr "Operation not supported"
msgid "eoverflow"
msgstr "Value too large for defined data type"
msgid "epipe"
msgstr "Broken pipe"
msgid "erange"
msgstr "Numerical result out of range"
msgid "erofs"
msgstr "Read-only file system"
msgid "espipe"
msgstr "Illegal seek"
msgid "esrch"
msgstr "No such process"
msgid "estale"
msgstr "Stale file handle"
msgid "etxtbsy"
msgstr "Text file busy"
msgid "exdev"
msgstr "Invalid cross-device link"

View File

@ -0,0 +1,149 @@
## This file is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here as no
## effect: edit them in PO (`.po`) files instead.
msgid "eperm"
msgstr ""
msgid "eacces"
msgstr ""
msgid "eagain"
msgstr ""
msgid "ebadf"
msgstr ""
msgid "ebadmsg"
msgstr ""
msgid "ebusy"
msgstr ""
msgid "edeadlk"
msgstr ""
msgid "edeadlock"
msgstr ""
msgid "edquot"
msgstr ""
msgid "eexist"
msgstr ""
msgid "efault"
msgstr ""
msgid "efbig"
msgstr ""
msgid "eftype"
msgstr ""
msgid "eintr"
msgstr ""
msgid "einval"
msgstr ""
msgid "eio"
msgstr ""
msgid "eisdir"
msgstr ""
msgid "eloop"
msgstr ""
msgid "emfile"
msgstr ""
msgid "emlink"
msgstr ""
msgid "emultihop"
msgstr ""
msgid "enametoolong"
msgstr ""
msgid "enfile"
msgstr ""
msgid "enobufs"
msgstr ""
msgid "enodev"
msgstr ""
msgid "enolck"
msgstr ""
msgid "enolink"
msgstr ""
msgid "enoent"
msgstr ""
msgid "enomem"
msgstr ""
msgid "enospc"
msgstr ""
msgid "enosr"
msgstr ""
msgid "enostr"
msgstr ""
msgid "enosys"
msgstr ""
msgid "enotblk"
msgstr ""
msgid "enotdir"
msgstr ""
msgid "enotsup"
msgstr ""
msgid "enxio"
msgstr ""
msgid "eopnotsupp"
msgstr ""
msgid "eoverflow"
msgstr ""
msgid "epipe"
msgstr ""
msgid "erange"
msgstr ""
msgid "erofs"
msgstr ""
msgid "espipe"
msgstr ""
msgid "esrch"
msgstr ""
msgid "estale"
msgstr ""
msgid "etxtbsy"
msgstr ""
msgid "exdev"
msgstr ""

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