diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 675d0e067..bfd9bf414 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -79,12 +79,12 @@ build-1.14.5-otp-25: script: - mix compile --force -build-1.17.1-otp-26: +build-1.18.3-otp-27: extends: - .build_changes_policy - .using-ci-base stage: build - image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.17.1-otp-26 + image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.18.3-otp-27 script: - mix compile --force @@ -142,12 +142,12 @@ unit-testing-1.14.5-otp-25: coverage_format: cobertura path: coverage.xml -unit-testing-1.17.1-otp-26: +unit-testing-1.18.3-otp-27: extends: - .build_changes_policy - .using-ci-base stage: test - image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.17.1-otp-26 + image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.18.3-otp-27 cache: *testing_cache_policy services: *testing_services script: *testing_script @@ -208,7 +208,7 @@ docs-deploy: before_script: - apk add curl script: - - curl --fail-with-body -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline + - curl --fail-with-body -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" https://git.pleroma.social/api/v4/projects/673/trigger/pipeline review_app: image: alpine:3.9 stage: deploy @@ -249,7 +249,7 @@ spec-deploy: before_script: - apk add curl script: - - curl --fail-with-body -X POST -F"token=$CI_JOB_TOKEN" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline + - curl --fail-with-body -X POST -F"token=$API_DOCS_PIPELINE_TRIGGER" -F'ref=master' -F"variables[BRANCH]=$CI_COMMIT_REF_NAME" -F"variables[JOB_REF]=$CI_JOB_ID" https://git.pleroma.social/api/v4/projects/1130/trigger/pipeline stop_review_app: diff --git a/changelog.d/activity_type_index.change b/changelog.d/activity_type_index.change new file mode 100644 index 000000000..ea2d7adbe --- /dev/null +++ b/changelog.d/activity_type_index.change @@ -0,0 +1 @@ +Add new activity actor/type index. Greatly speeds up retrieval of rare types (like "Listen") diff --git a/changelog.d/admin-api-docs-fix.skip b/changelog.d/admin-api-docs-fix.skip new file mode 100644 index 000000000..5c1c68ea0 --- /dev/null +++ b/changelog.d/admin-api-docs-fix.skip @@ -0,0 +1 @@ +Fix 'Create a user' description in admin api docs diff --git a/changelog.d/admin-api-log-fix.skip b/changelog.d/admin-api-log-fix.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/assign-app-user-oom.fix b/changelog.d/assign-app-user-oom.fix new file mode 100644 index 000000000..ac1de7159 --- /dev/null +++ b/changelog.d/assign-app-user-oom.fix @@ -0,0 +1 @@ +Fix AssignAppUser migration OOM diff --git a/changelog.d/deepl-json.fix b/changelog.d/deepl-json.fix new file mode 100644 index 000000000..ee6f8664e --- /dev/null +++ b/changelog.d/deepl-json.fix @@ -0,0 +1 @@ +Use JSON for DeepL API requests diff --git a/changelog.d/dislike-activity.add b/changelog.d/dislike-activity.add new file mode 100644 index 000000000..1fcbda78b --- /dev/null +++ b/changelog.d/dislike-activity.add @@ -0,0 +1 @@ +Support Dislike activity, as sent by Mitra and Friendica, by changing it into a thumbs-down EmojiReact \ No newline at end of file diff --git a/changelog.d/doc-typo.skip b/changelog.d/doc-typo.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/elixir-1-18.fix b/changelog.d/elixir-1-18.fix new file mode 100644 index 000000000..d4d5a3493 --- /dev/null +++ b/changelog.d/elixir-1-18.fix @@ -0,0 +1 @@ +Elixir 1.18: Fixed warnings and new deprecations diff --git a/changelog.d/emoji_likes.add b/changelog.d/emoji_likes.add new file mode 100644 index 000000000..13c91a950 --- /dev/null +++ b/changelog.d/emoji_likes.add @@ -0,0 +1 @@ +Support Mitra-style emoji likes. diff --git a/changelog.d/expiring-blocks.add b/changelog.d/expiring-blocks.add new file mode 100644 index 000000000..29989af15 --- /dev/null +++ b/changelog.d/expiring-blocks.add @@ -0,0 +1 @@ +Add `duration` to the block endpoint, which makes block expire \ No newline at end of file diff --git a/changelog.d/expose-markup-configuration.add b/changelog.d/expose-markup-configuration.add new file mode 100644 index 000000000..8c7f35697 --- /dev/null +++ b/changelog.d/expose-markup-configuration.add @@ -0,0 +1 @@ +Expose markup configuration in InstanceView diff --git a/changelog.d/fixtests.skip b/changelog.d/fixtests.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/freebsd-rc.fix b/changelog.d/freebsd-rc.fix new file mode 100644 index 000000000..1f59d4596 --- /dev/null +++ b/changelog.d/freebsd-rc.fix @@ -0,0 +1 @@ +Set PATH in the FreeBSD rc script to avoid failures starting the service diff --git a/changelog.d/gin-search.fix b/changelog.d/gin-search.fix new file mode 100644 index 000000000..ba9977b6e --- /dev/null +++ b/changelog.d/gin-search.fix @@ -0,0 +1 @@ +Improved performance of status search queries using the default GIN index diff --git a/changelog.d/language-detection.add b/changelog.d/language-detection.add new file mode 100644 index 000000000..6d1a7f705 --- /dev/null +++ b/changelog.d/language-detection.add @@ -0,0 +1 @@ +Implement language detection with fastText \ No newline at end of file diff --git a/changelog.d/openbsd-docs-update.skip b/changelog.d/openbsd-docs-update.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/openbsd-update-httpd-relayd.change b/changelog.d/openbsd-update-httpd-relayd.change new file mode 100644 index 000000000..2ee85c2b0 --- /dev/null +++ b/changelog.d/openbsd-update-httpd-relayd.change @@ -0,0 +1 @@ +Updated relayd/httpd config files to be on par with nginx diff --git a/changelog.d/openbsd-update-rc.fix b/changelog.d/openbsd-update-rc.fix new file mode 100644 index 000000000..2d4263827 --- /dev/null +++ b/changelog.d/openbsd-update-rc.fix @@ -0,0 +1 @@ +replaced depracated flags and functions, renamed service to fit other service files diff --git a/changelog.d/preserve-public-cc.fix b/changelog.d/preserve-public-cc.fix new file mode 100644 index 000000000..1b20ce9ad --- /dev/null +++ b/changelog.d/preserve-public-cc.fix @@ -0,0 +1 @@ +Fix federation issue where Public visibility information in cc field was lost when sent to remote servers, causing posts to appear with inconsistent visibility across instances diff --git a/changelog.d/private-functions.skip b/changelog.d/private-functions.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/relax-also-known-as.change b/changelog.d/relax-also-known-as.change new file mode 100644 index 000000000..800c3e72a --- /dev/null +++ b/changelog.d/relax-also-known-as.change @@ -0,0 +1 @@ +Relax alsoKnownAs requirements to just URI, not necessarily HTTP(S) \ No newline at end of file diff --git a/changelog.d/releases.fix b/changelog.d/releases.fix new file mode 100644 index 000000000..5436accc7 --- /dev/null +++ b/changelog.d/releases.fix @@ -0,0 +1 @@ +Fix release builds diff --git a/changelog.d/remove-forgotten-OTPVersion-usage.skip b/changelog.d/remove-forgotten-OTPVersion-usage.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/scrobbles.change b/changelog.d/scrobbles.change new file mode 100644 index 000000000..ed1777b2d --- /dev/null +++ b/changelog.d/scrobbles.change @@ -0,0 +1 @@ +Change scrobble external link param name to use snake case \ No newline at end of file diff --git a/changelog.d/siteinfo-baseurls.add b/changelog.d/siteinfo-baseurls.add new file mode 100644 index 000000000..6f0f19847 --- /dev/null +++ b/changelog.d/siteinfo-baseurls.add @@ -0,0 +1 @@ +Add `base_urls` to the /api/v1/instance pleroma metadata which provides information about the base URLs for media_proxy and uploads when configured \ No newline at end of file diff --git a/changelog.d/toctou-mkdir.fix b/changelog.d/toctou-mkdir.fix new file mode 100644 index 000000000..b070db1a0 --- /dev/null +++ b/changelog.d/toctou-mkdir.fix @@ -0,0 +1 @@ +Backport [Elixir PR 14242](https://github.com/elixir-lang/elixir/pull/14242) fixing racy mkdir and lack of error handling of parent directory creation \ No newline at end of file diff --git a/changelog.d/tos-setting.add b/changelog.d/tos-setting.add new file mode 100644 index 000000000..db9b0d5f2 --- /dev/null +++ b/changelog.d/tos-setting.add @@ -0,0 +1 @@ +Allow Terms of Service panel behaviour to be configurable diff --git a/changelog.d/translate-posts.add b/changelog.d/translate-posts.add new file mode 100644 index 000000000..e7a9317a1 --- /dev/null +++ b/changelog.d/translate-posts.add @@ -0,0 +1 @@ +Support translation providers (DeepL, LibreTranslate) \ No newline at end of file diff --git a/changelog.d/truncate-rich-media.change b/changelog.d/truncate-rich-media.change new file mode 100644 index 000000000..1df064be1 --- /dev/null +++ b/changelog.d/truncate-rich-media.change @@ -0,0 +1 @@ +Truncate the length of Rich Media title and description fields diff --git a/changelog.d/typos.skip b/changelog.d/typos.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/webfinger.change b/changelog.d/webfinger.change new file mode 100644 index 000000000..353e65a89 --- /dev/null +++ b/changelog.d/webfinger.change @@ -0,0 +1 @@ +Don't require an Accept header for WebFinger queries and default to JSON. \ No newline at end of file diff --git a/ci/elixir-1.18.3-otp-27/Dockerfile b/ci/elixir-1.18.3-otp-27/Dockerfile new file mode 100644 index 000000000..2b42aa90d --- /dev/null +++ b/ci/elixir-1.18.3-otp-27/Dockerfile @@ -0,0 +1,8 @@ +FROM elixir:1.18.3-otp-27 + +# Single RUN statement, otherwise intermediate images are created +# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run +RUN apt-get update &&\ + apt-get install -y libmagic-dev cmake libimage-exiftool-perl ffmpeg &&\ + mix local.hex --force &&\ + mix local.rebar --force diff --git a/ci/elixir-1.18.3-otp-27/build_and_push.sh b/ci/elixir-1.18.3-otp-27/build_and_push.sh new file mode 100755 index 000000000..8a564fbf2 --- /dev/null +++ b/ci/elixir-1.18.3-otp-27/build_and_push.sh @@ -0,0 +1 @@ +docker buildx build --platform linux/amd64,linux/arm64 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.18.3-otp-27 --push . diff --git a/config/config.exs b/config/config.exs index d90d56d9b..b211d0776 100644 --- a/config/config.exs +++ b/config/config.exs @@ -48,7 +48,7 @@ config :pleroma, ecto_repos: [Pleroma.Repo] config :pleroma, Pleroma.Repo, telemetry_event: [Pleroma.Repo.Instrumenter], - migration_lock: nil + migration_lock: :pg_advisory_lock config :pleroma, Pleroma.Captcha, enabled: true, @@ -305,6 +305,7 @@ config :pleroma, :frontend_configurations, collapseMessageWithSubject: false, disableChat: false, greentext: false, + embeddedToS: true, hideFilteredStatuses: false, hideMutedPosts: false, hidePostStats: false, diff --git a/config/description.exs b/config/description.exs index 996978298..e20fa4b28 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1261,6 +1261,7 @@ config :pleroma, :config_description, [ background: "/static/aurora_borealis.jpg", collapseMessageWithSubject: false, greentext: false, + embeddedToS: true, hideFilteredStatuses: false, hideMutedPosts: false, hidePostStats: false, @@ -1312,6 +1313,12 @@ config :pleroma, :config_description, [ type: :boolean, description: "Enables green text on lines prefixed with the > character" }, + %{ + key: :embeddedToS, + label: "Embedded ToS panel", + type: :boolean, + description: "Hide Terms of Service panel decorations on About and Registration pages" + }, %{ key: :hideFilteredStatuses, label: "Hide Filtered Statuses", @@ -3500,5 +3507,71 @@ config :pleroma, :config_description, [ suggestion: [100_000] } ] + }, + %{ + group: :pleroma, + key: Pleroma.Language.LanguageDetector, + type: :group, + description: "Language detection providers", + children: [ + %{ + key: :provider, + type: :module, + suggestions: [ + Pleroma.Language.LanguageDetector.Fasttext + ] + }, + %{ + group: {:subgroup, Pleroma.Language.LanguageDetector.Fasttext}, + key: :model, + label: "fastText language detection model", + type: :string, + suggestions: ["/usr/share/fasttext/lid.176.bin"] + } + ] + }, + %{ + group: :pleroma, + key: Pleroma.Language.Translation, + type: :group, + description: "Translation providers", + children: [ + %{ + key: :provider, + type: :module, + suggestions: [ + Pleroma.Language.Translation.Deepl, + Pleroma.Language.Translation.Libretranslate + ] + }, + %{ + group: {:subgroup, Pleroma.Language.Translation.Deepl}, + key: :base_url, + label: "DeepL base URL", + type: :string, + suggestions: ["https://api-free.deepl.com", "https://api.deepl.com"] + }, + %{ + group: {:subgroup, Pleroma.Language.Translation.Deepl}, + key: :api_key, + label: "DeepL API Key", + type: :string, + suggestions: ["YOUR_API_KEY"] + }, + %{ + group: {:subgroup, Pleroma.Language.Translation.Libretranslate}, + key: :base_url, + label: "LibreTranslate instance URL", + type: :string, + suggestions: ["https://libretranslate.com"] + }, + %{ + group: {:subgroup, Pleroma.Language.Translation.Libretranslate}, + key: :api_key, + label: "LibreTranslate API Key", + type: :string, + suggestions: ["YOUR_API_KEY"] + } + ] } ] diff --git a/config/test.exs b/config/test.exs index 0f8b12ffe..fefdc2bbc 100644 --- a/config/test.exs +++ b/config/test.exs @@ -156,6 +156,7 @@ config :pleroma, Pleroma.User.Backup, config_impl: Pleroma.UnstubbedConfigMock config :pleroma, Pleroma.Uploaders.S3, ex_aws_impl: Pleroma.Uploaders.S3.ExAwsMock config :pleroma, Pleroma.Uploaders.S3, config_impl: Pleroma.UnstubbedConfigMock config :pleroma, Pleroma.Upload, config_impl: Pleroma.UnstubbedConfigMock +config :pleroma, Pleroma.Language.LanguageDetector, config_impl: Pleroma.StaticStubbedConfigMock config :pleroma, Pleroma.ScheduledActivity, config_impl: Pleroma.UnstubbedConfigMock config :pleroma, Pleroma.Web.RichMedia.Helpers, config_impl: Pleroma.StaticStubbedConfigMock config :pleroma, Pleroma.Uploaders.IPFS, config_impl: Pleroma.UnstubbedConfigMock diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md index 409e78a1e..64c06ca2b 100644 --- a/docs/development/API/admin_api.md +++ b/docs/development/API/admin_api.md @@ -70,6 +70,8 @@ The `/api/v1/pleroma/admin/*` path is backwards compatible with `/api/pleroma/ad - `nicknames` - Response: Array of user nicknames +## `POST /api/v1/pleroma/admin/users` + ### Create a user - Method: `POST` @@ -81,7 +83,7 @@ The `/api/v1/pleroma/admin/*` path is backwards compatible with `/api/pleroma/ad `password` } ] -- Response: User’s nickname +- Response: Array of user objects ## `POST /api/v1/pleroma/admin/users/follow` diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md index 000d7d27d..b17f61cbb 100644 --- a/docs/development/API/pleroma_api.md +++ b/docs/development/API/pleroma_api.md @@ -671,6 +671,7 @@ Audio scrobbling in Pleroma is **deprecated**. "artist": "Some Artist", "album": "Some Album", "length": 180000, + "external_link": "https://www.last.fm/music/Some+Artist/_/Some+Title", "created_at": "2019-09-28T12:40:45.000Z" } ] diff --git a/docs/development/index.md b/docs/development/index.md index 01a617596..6b35321c5 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -1 +1,7 @@ This section contains notes and guidelines for developers. + +- [Setting up a Pleroma development environment](setting_up_pleroma_dev.md) +- [Setting up a Gitlab Runner](setting_up_a_gitlab_runner.md) +- [Authentication & Authorization](authentication_authorization.md) +- [ActivityPub Extensions](ap_extensions.md) +- [Mox Testing Guide](mox_testing.md) diff --git a/docs/development/mox_testing.md b/docs/development/mox_testing.md new file mode 100644 index 000000000..673064022 --- /dev/null +++ b/docs/development/mox_testing.md @@ -0,0 +1,485 @@ +# Using Mox for Testing in Pleroma + +## Introduction + +This guide explains how to use [Mox](https://hexdocs.pm/mox/Mox.html) for testing in Pleroma and how to migrate existing tests from Mock/meck to Mox. Mox is a library for defining concurrent mocks in Elixir that offers several key advantages: + +- **Async-safe testing**: Mox supports concurrent testing with `async: true` +- **Explicit contract through behaviors**: Enforces implementation of behavior callbacks +- **No module redefinition**: Avoids runtime issues caused by redefining modules +- **Expectations scoped to the current process**: Prevents test state from leaking between tests + +## Why Migrate from Mock/meck to Mox? + +### Problems with Mock/meck + +1. **Not async-safe**: Tests using Mock/meck cannot safely run with `async: true`, which slows down the test suite +2. **Global state**: Mocked functions are global, leading to potential cross-test contamination +3. **No explicit contract**: No guarantee that mocked functions match the actual implementation +4. **Module redefinition**: Can lead to hard-to-debug runtime issues + +### Benefits of Mox + +1. **Async-safe testing**: Tests can run concurrently with `async: true`, significantly speeding up the test suite +2. **Process isolation**: Expectations are set per process, preventing leakage between tests +3. **Explicit contracts via behaviors**: Ensures mocks implement all required functions +4. **Compile-time checks**: Prevents mocking non-existent functions +5. **No module redefinition**: Mocks are defined at compile time, not runtime + +## Existing Mox Setup in Pleroma + +Pleroma already has a basic Mox setup in the `Pleroma.DataCase` module, which handles some common mocking scenarios automatically. Here's what's included: + +### Default Mox Configuration + +The `setup` function in `DataCase` does the following: + +1. Sets up Mox for either async or non-async tests +2. Verifies all mock expectations on test exit +3. Stubs common dependencies with their real implementations + +```elixir +# From test/support/data_case.ex +setup tags do + setup_multi_process_mode(tags) + setup_streamer(tags) + stub_pipeline() + + Mox.verify_on_exit!() + + :ok +end +``` + +### Async vs. Non-Async Test Setup + +Pleroma configures Mox differently depending on whether your test is async or not: + +```elixir +def setup_multi_process_mode(tags) do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo) + + if tags[:async] do + # For async tests, use process-specific mocks and stub CachexMock with NullCache + Mox.stub_with(Pleroma.CachexMock, Pleroma.NullCache) + Mox.set_mox_private() + else + # For non-async tests, use global mocks and stub CachexMock with CachexProxy + Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, {:shared, self()}) + + Mox.set_mox_global() + Mox.stub_with(Pleroma.CachexMock, Pleroma.CachexProxy) + clear_cachex() + end + + :ok +end +``` + +### Default Pipeline Stubs + +Pleroma automatically stubs several core components with their real implementations: + +```elixir +def stub_pipeline do + Mox.stub_with(Pleroma.Web.ActivityPub.SideEffectsMock, Pleroma.Web.ActivityPub.SideEffects) + Mox.stub_with(Pleroma.Web.ActivityPub.ObjectValidatorMock, Pleroma.Web.ActivityPub.ObjectValidator) + Mox.stub_with(Pleroma.Web.ActivityPub.MRFMock, Pleroma.Web.ActivityPub.MRF) + Mox.stub_with(Pleroma.Web.ActivityPub.ActivityPubMock, Pleroma.Web.ActivityPub.ActivityPub) + Mox.stub_with(Pleroma.Web.FederatorMock, Pleroma.Web.Federator) + Mox.stub_with(Pleroma.ConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Test.StaticConfig) + Mox.stub_with(Pleroma.StubbedHTTPSignaturesMock, Pleroma.Test.HTTPSignaturesProxy) +end +``` + +This means that by default, these mocks will behave like their real implementations unless you explicitly override them with expectations in your tests. + +### Understanding Config Mock Types + +Pleroma has three different Config mock implementations, each with a specific purpose and different characteristics regarding async test safety: + +#### 1. ConfigMock + +- Defined in `test/support/mocks.ex` as `Mox.defmock(Pleroma.ConfigMock, for: Pleroma.Config.Getting)` +- It's stubbed with the real `Pleroma.Config` by default in `DataCase`: `Mox.stub_with(Pleroma.ConfigMock, Pleroma.Config)` +- This means it falls back to the normal configuration behavior unless explicitly overridden +- Used for general mocking of configuration in tests where you want most config to behave normally +- ⚠️ **NOT ASYNC-SAFE**: Since it's stubbed with the real `Pleroma.Config`, it modifies global application state +- Can not be used in tests with `async: true` + +#### 2. StaticStubbedConfigMock + +- Defined in `test/support/mocks.ex` as `Mox.defmock(Pleroma.StaticStubbedConfigMock, for: Pleroma.Config.Getting)` +- It's stubbed with `Pleroma.Test.StaticConfig` (defined in `test/test_helper.exs`) +- `Pleroma.Test.StaticConfig` creates a completely static configuration snapshot at the start of the test run: + ```elixir + defmodule Pleroma.Test.StaticConfig do + @moduledoc """ + This module provides a Config that is completely static, built at startup time from the environment. + It's safe to use in testing as it will not modify any state. + """ + + @behaviour Pleroma.Config.Getting + @config Application.get_all_env(:pleroma) + + def get(path, default \\ nil) do + get_in(@config, path) || default + end + end + ``` +- Configuration is frozen at startup time and doesn't change during the test run +- ✅ **ASYNC-SAFE**: Never modifies global state since it uses a frozen snapshot of the configuration + +#### 3. UnstubbedConfigMock + +- Defined in `test/support/mocks.ex` as `Mox.defmock(Pleroma.UnstubbedConfigMock, for: Pleroma.Config.Getting)` +- Unlike the other two mocks, it's not automatically stubbed with any implementation in `DataCase` +- Starts completely "unstubbed" and requires tests to explicitly set expectations or stub it +- The most commonly used configuration mock in the test suite +- Often aliased as `ConfigMock` in individual test files: `alias Pleroma.UnstubbedConfigMock, as: ConfigMock` +- Set as the default config implementation in `config/test.exs`: `config :pleroma, :config_impl, Pleroma.UnstubbedConfigMock` +- Offers maximum flexibility for tests that need precise control over configuration values +- ✅ **ASYNC-SAFE**: Safe if used with `expect()` to set up test-specific expectations (since expectations are process-scoped) + +#### Configuring Components to Use Specific Mocks + +In `config/test.exs`, different components can be configured to use different configuration mocks: + +```elixir +# Components using UnstubbedConfigMock +config :pleroma, Pleroma.Upload, config_impl: Pleroma.UnstubbedConfigMock +config :pleroma, Pleroma.User.Backup, config_impl: Pleroma.UnstubbedConfigMock +config :pleroma, Pleroma.Uploaders.S3, config_impl: Pleroma.UnstubbedConfigMock + +# Components using StaticStubbedConfigMock (async-safe) +config :pleroma, Pleroma.Language.LanguageDetector, config_impl: Pleroma.StaticStubbedConfigMock +config :pleroma, Pleroma.Web.RichMedia.Helpers, config_impl: Pleroma.StaticStubbedConfigMock +config :pleroma, Pleroma.Web.Plugs.HTTPSecurityPlug, config_impl: Pleroma.StaticStubbedConfigMock +``` + +This allows different parts of the application to use the most appropriate configuration mocking strategy based on their specific needs. + +#### When to Use Each Config Mock Type + +- **ConfigMock**: ⚠️ For non-async tests only, when you want most configuration to behave normally with occasional overrides +- **StaticStubbedConfigMock**: ✅ For async tests where modifying global state would be problematic and a static configuration is sufficient +- **UnstubbedConfigMock**: ⚠️ Use carefully in async tests; set specific expectations rather than stubbing with implementations that modify global state + +#### Summary of Async Safety + +| Mock Type | Async-Safe? | Best Use Case | +|-----------|-------------|--------------| +| ConfigMock | ❌ No | Non-async tests that need minimal configuration overrides | +| StaticStubbedConfigMock | ✅ Yes | Async tests that need configuration values without modification | +| UnstubbedConfigMock | ⚠️ Depends | Any test with careful usage; set expectations rather than stubbing | + +## Configuration in Async Tests + +### Understanding `clear_config` Limitations + +The `clear_config` helper is commonly used in Pleroma tests to modify configuration for specific tests. However, it's important to understand that **`clear_config` is not async-safe** and should not be used in tests with `async: true`. + +Here's why: + +```elixir +# Implementation of clear_config in test/support/helpers.ex +defmacro clear_config(config_path, temp_setting) do + quote do + clear_config(unquote(config_path)) do + Config.put(unquote(config_path), unquote(temp_setting)) + end + end +end + +defmacro clear_config(config_path, do: yield) do + quote do + initial_setting = Config.fetch(unquote(config_path)) + + unquote(yield) + + on_exit(fn -> + case initial_setting do + :error -> + Config.delete(unquote(config_path)) + + {:ok, value} -> + Config.put(unquote(config_path), value) + end + end) + + :ok + end +end +``` + +The issue is that `clear_config`: +1. Modifies the global application environment +2. Uses `on_exit` to restore the original value after the test +3. Can lead to race conditions when multiple async tests modify the same configuration + +### Async-Safe Configuration Approaches + +When writing async tests with Mox, use these approaches instead of `clear_config`: + +1. **Dependency Injection with Module Attributes**: + ```elixir + # In your module + @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) + + def some_function do + value = @config_impl.get([:some, :config]) + # ... + end + ``` + +2. **Mock the Config Module**: + ```elixir + # In your test + Pleroma.ConfigMock + |> expect(:get, fn [:some, :config] -> "test_value" end) + ``` + +3. **Use Test-Specific Implementations**: + ```elixir + # Define a test-specific implementation + defmodule TestConfig do + def get([:some, :config]), do: "test_value" + def get(_), do: nil + end + + # In your test + Mox.stub_with(Pleroma.ConfigMock, TestConfig) + ``` + +4. **Pass Configuration as Arguments**: + ```elixir + # Refactor functions to accept configuration as arguments + def some_function(config \\ nil) do + config = config || Pleroma.Config.get([:some, :config]) + # ... + end + + # In your test + some_function("test_value") + ``` + +By using these approaches, you can safely run tests with `async: true` without worrying about configuration conflicts. + +## Setting Up Mox in Pleroma + +### Step 1: Define a Behavior + +Start by defining a behavior for the module you want to mock. This specifies the contract that both the real implementation and mocks must follow. + +```elixir +# In your implementation module (e.g., lib/pleroma/uploaders/s3.ex) +defmodule Pleroma.Uploaders.S3.ExAwsAPI do + @callback request(op :: ExAws.Operation.t()) :: {:ok, ExAws.Operation.t()} | {:error, term()} +end +``` + +### Step 2: Make Your Implementation Configurable + +Modify your module to use a configurable implementation. This allows for dependency injection and easier testing. + +```elixir +# In your implementation module +@ex_aws_impl Application.compile_env(:pleroma, [__MODULE__, :ex_aws_impl], ExAws) +@config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) + +def put_file(%Pleroma.Upload{} = upload) do + # Use @ex_aws_impl instead of ExAws directly + case @ex_aws_impl.request(op) do + {:ok, _} -> + {:ok, {:file, s3_name}} + + error -> + Logger.error("#{__MODULE__}: #{inspect(error)}") + error + end +end +``` + +### Step 3: Define the Mock in test/support/mocks.ex + +Add your mock definition in the central mocks file: + +```elixir +# In test/support/mocks.ex +Mox.defmock(Pleroma.Uploaders.S3.ExAwsMock, for: Pleroma.Uploaders.S3.ExAwsAPI) +``` + +### Step 4: Configure the Mock in Test Environment + +In your test configuration (e.g., `config/test.exs`), specify which mock implementation to use: + +```elixir +config :pleroma, Pleroma.Uploaders.S3, ex_aws_impl: Pleroma.Uploaders.S3.ExAwsMock +config :pleroma, Pleroma.Uploaders.S3, config_impl: Pleroma.UnstubbedConfigMock +``` + +## Writing Tests with Mox + +### Setting Up Your Test + +```elixir +defmodule Pleroma.Uploaders.S3Test do + use Pleroma.DataCase, async: true # Note: async: true is now possible! + + alias Pleroma.Uploaders.S3 + alias Pleroma.Uploaders.S3.ExAwsMock + alias Pleroma.UnstubbedConfigMock, as: ConfigMock + + import Mox # Import Mox functions + + # Note: verify_on_exit! is already called in DataCase setup + # so you don't need to add it explicitly in your test module +end +``` + +### Setting Expectations with Mox + +Mox uses an explicit expectation system. Here's how to use it: + +```elixir +# Basic expectation for a function call +ExAwsMock +|> expect(:request, fn _req -> {:ok, %{status_code: 200}} end) + +# Expectation for multiple calls with same response +ExAwsMock +|> expect(:request, 3, fn _req -> {:ok, %{status_code: 200}} end) + +# Expectation with specific arguments +ExAwsMock +|> expect(:request, fn %{bucket: "test_bucket"} -> {:ok, %{status_code: 200}} end) + +# Complex configuration mocking +ConfigMock +|> expect(:get, fn key -> + [ + {Pleroma.Upload, [uploader: Pleroma.Uploaders.S3, base_url: "https://s3.amazonaws.com"]}, + {Pleroma.Uploaders.S3, [bucket: "test_bucket"]} + ] + |> get_in(key) +end) +``` + +### Understanding Mox Modes in Pleroma + +Pleroma's DataCase automatically configures Mox differently based on whether your test is async or not: + +1. **Async tests** (`async: true`): + - Uses `Mox.set_mox_private()` - expectations are scoped to the current process + - Stubs `Pleroma.CachexMock` with `Pleroma.NullCache` + - Each test process has its own isolated mock expectations + +2. **Non-async tests** (`async: false`): + - Uses `Mox.set_mox_global()` - expectations are shared across processes + - Stubs `Pleroma.CachexMock` with `Pleroma.CachexProxy` + - Mock expectations can be set in one process and called from another + +Choose the appropriate mode based on your test requirements. For most tests, async mode is preferred for better performance. + +## Migrating from Mock/meck to Mox + +Here's a step-by-step guide for migrating existing tests from Mock/meck to Mox: + +### 1. Identify the Module to Mock + +Look for `with_mock` or `test_with_mock` calls in your tests: + +```elixir +# Old approach with Mock +with_mock ExAws, request: fn _ -> {:ok, :ok} end do + assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}} +end +``` + +### 2. Define a Behavior for the Module + +Create a behavior that defines the functions you want to mock: + +```elixir +defmodule Pleroma.Uploaders.S3.ExAwsAPI do + @callback request(op :: ExAws.Operation.t()) :: {:ok, ExAws.Operation.t()} | {:error, term()} +end +``` + +### 3. Update Your Implementation to Use a Configurable Dependency + +```elixir +# Old +def put_file(%Pleroma.Upload{} = upload) do + case ExAws.request(op) do + # ... + end +end + +# New +@ex_aws_impl Application.compile_env(:pleroma, [__MODULE__, :ex_aws_impl], ExAws) + +def put_file(%Pleroma.Upload{} = upload) do + case @ex_aws_impl.request(op) do + # ... + end +end +``` + +### 4. Define the Mock in mocks.ex + +```elixir +Mox.defmock(Pleroma.Uploaders.S3.ExAwsMock, for: Pleroma.Uploaders.S3.ExAwsAPI) +``` + +### 5. Configure the Test Environment + +```elixir +config :pleroma, Pleroma.Uploaders.S3, ex_aws_impl: Pleroma.Uploaders.S3.ExAwsMock +``` + +### 6. Update Your Tests to Use Mox + +```elixir +# Old (with Mock) +test_with_mock "save file", ExAws, request: fn _ -> {:ok, :ok} end do + assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}} + assert_called(ExAws.request(:_)) +end + +# New (with Mox) +test "save file" do + ExAwsMock + |> expect(:request, fn _req -> {:ok, %{status_code: 200}} end) + + assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}} +end +``` + +### 7. Enable Async Testing + +Now you can safely enable `async: true` in your test module: + +```elixir +use Pleroma.DataCase, async: true +``` + +## Best Practices + +1. **Always define behaviors**: They serve as contracts and documentation +2. **Keep mocks in a central location**: Use test/support/mocks.ex for all mock definitions +3. **Use verify_on_exit!**: This is already set up in DataCase, ensuring all expected calls were made +4. **Use specific expectations**: Be as specific as possible with your expectations +5. **Enable async: true**: Take advantage of Mox's concurrent testing capability +6. **Don't over-mock**: Only mock external dependencies that are difficult to test directly +7. **Leverage existing stubs**: Use the default stubs provided by DataCase when possible +8. **Avoid clear_config in async tests**: Use dependency injection and mocking instead + +## Example: Complete Migration + +For a complete example of migrating a test from Mock/meck to Mox, you can refer to commit `90a47ca050c5839e8b4dc3bac315dc436d49152d` in the Pleroma repository, which shows how the S3 uploader tests were migrated. + +## Conclusion + +Migrating tests from Mock/meck to Mox provides significant benefits for the Pleroma test suite, including faster test execution through async testing, better isolation between tests, and more robust mocking through explicit contracts. By following this guide, you can successfully migrate existing tests and write new tests using Mox. \ No newline at end of file diff --git a/docs/installation/generic_dependencies.include b/docs/installation/generic_dependencies.include index 9f07f62c6..769347a3c 100644 --- a/docs/installation/generic_dependencies.include +++ b/docs/installation/generic_dependencies.include @@ -1,8 +1,8 @@ ## Required dependencies * PostgreSQL >=11.0 -* Elixir >=1.14.0 <1.17 -* Erlang OTP >=23.0.0 (supported: <27) +* Elixir >=1.14.0 <1.19 +* Erlang OTP >=23.0.0 (supported: <28) * git * file / libmagic * gcc or clang diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md index 78bbf399f..1de016cdd 100644 --- a/docs/installation/openbsd_en.md +++ b/docs/installation/openbsd_en.md @@ -1,25 +1,29 @@ # Installing on OpenBSD -This guide describes the installation and configuration of pleroma (and the required software to run it) on a single OpenBSD 6.6 server. +{! backend/installation/otp_vs_from_source_source.include !} + +This guide describes the installation and configuration of Pleroma (and the required software to run it) on a single OpenBSD 7.7 server. For any additional information regarding commands and configuration files mentioned here, check the man pages [online](https://man.openbsd.org/) or directly on your server with the man command. {! backend/installation/generic_dependencies.include !} +## Installation + ### Preparing the system #### Required software -To install them, run the following command (with doas or as root): +To install required packages, run the following command: ``` -pkg_add elixir gmake git postgresql-server postgresql-contrib cmake ffmpeg ImageMagick libvips +# pkg_add elixir gmake git postgresql-server postgresql-contrib cmake libmagic libvips ``` -Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt. +Pleroma requires a reverse proxy, OpenBSD has relayd in base (and is used in this guide) and packages/ports are available for nginx (www/nginx) and apache (www/apache-httpd). +Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acme-client) can be used to get a certificate from Let's Encrypt. #### Optional software -Per [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md): * ImageMagick * ffmpeg * exiftool @@ -27,234 +31,351 @@ Per [`docs/installation/optional/media_graphics_packages.md`](../installation/op To install the above: ``` -pkg_add ImageMagick ffmpeg p5-Image-ExifTool +# pkg_add ImageMagick ffmpeg p5-Image-ExifTool ``` -#### Creating the pleroma user -Pleroma will be run by a dedicated user, \_pleroma. Before creating it, insert the following lines in login.conf: +For more information read [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md): + +### PostgreSQL + +Switch to the \_postgresql user and initialize PostgreSQL: + +``` +# su _postgresql +$ initdb -D /var/postgresql/data -U postgres --encoding=utf-8 --lc-collate=C +``` + +Running PostgreSQL in a different directory than `/var/postgresql/data` requires changing the `daemon_flags` variable in the `/etc/rc.d/postgresql` script. + +For security reasons it is recommended to change the authentication method for `local` and `host` connections with the localhost address to `scram-sha-256`.
+Do not forget to set a password for the `postgres` user before doing so, otherwise you won't be able to log back in unless you change the authentication method back to `trust`.
+Changing the password hashing algorithm is not needed.
+For more information [read](https://www.postgresql.org/docs/16/auth-pg-hba-conf.html) the PostgreSQL documentation. + +Enable and start the postgresql service: + +``` +# rcctl enable postgresql +# rcctl start postgresql +``` + +To check that PostgreSQL started properly and didn't fail right after starting, run `# rcctl check postgresql` which should return `postgresql(ok)`. + +### Configuring Pleroma + +Pleroma will be run by a dedicated \_pleroma user. Before creating it, insert the following lines in `/etc/login.conf`: + ``` pleroma:\ - :datasize-max=1536M:\ - :datasize-cur=1536M:\ - :openfiles-max=4096 + :datasize=1536M:\ + :openfiles-max=4096:\ + :openfiles-cur=1024:\ + :setenv=LC_ALL=en_US.UTF-8,VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS,MIX_ENV=prod:\ + :tc=daemon: ``` -This creates a "pleroma" login class and sets higher values than default for datasize and openfiles (see [login.conf(5)](https://man.openbsd.org/login.conf)), this is required to avoid having pleroma crash some time after starting. -Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): `useradd -m -L pleroma _pleroma` +This creates a "pleroma" login class and sets higher values than default for datasize and openfiles (see [login.conf(5)](https://man.openbsd.org/login.conf)), this is required to avoid having Pleroma crash some time after starting. -#### Clone pleroma's directory -Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide. - -#### PostgreSQL -Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql: -You will need to specify pgdata directory to the default (/var/postgresql/data) with the `-D ` and set the user to postgres with the `-U ` flag. This can be done as follows: +Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): ``` -initdb -D /var/postgresql/data -U postgres +# useradd -m -L pleroma _pleroma ``` -If you are not using the default directory, you will have to update the `datadir` variable in the /etc/rc.d/postgresql script. -When this is done, enable postgresql so that it starts on boot and start it. As root, run: +Switch to the _pleroma user: + ``` -rcctl enable postgresql -rcctl start postgresql +# su -l _pleroma +``` + +Clone the Pleroma repository: + +``` +$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git +$ cd pleroma +``` + +Pleroma is now installed in /home/\_pleroma/pleroma/. To configure it run: + +``` +$ mix deps.get +$ MIX_ENV=prod mix pleroma.instance gen # You will be asked a few questions here. +$ cp config/generated_config.exs config/prod.secret.exs +``` + +Note: Answer yes when asked to install Hex and rebar3. This step might take some time as Pleroma gets compiled first. + +Create the Pleroma database: + +``` +$ psql -U postgres -f config/setup_db.psql +``` + +Apply database migrations: + +``` +$ MIX_ENV=prod mix ecto.migrate +``` + +Note: You will need to run this step again when updating your instance to a newer version with `git pull` or `git checkout tags/NEW_VERSION`. + +As \_pleroma in /home/\_pleroma/pleroma, you can now run `MIX_ENV=prod mix phx.server` to start your instance. +In another SSH session or a tmux window, check that it is working properly by running `ftp -MVo - http://127.0.0.1:4000/api/v1/instance`, you should get json output. +Double-check that the *uri* value near the bottom is your instance's domain name and the instance *title* are correct. + +### Configuring acme-client + +acme-client is used to get SSL/TLS certificates from Let's Encrypt. +Insert the following configuration in `/etc/acme-client.conf` and replace `example.tld` with your domain: + +``` +# +# $OpenBSD: acme-client.conf,v 1.5 2023/05/10 07:34:57 tb Exp $ +# + +authority letsencrypt { + api url "https://acme-v02.api.letsencrypt.org/directory" + account key "/etc/acme/letsencrypt-privkey.pem" +} + +domain example.tld { + # Adds alternative names to the certificate. Useful when serving media on another domain. Comma or space separated list. + # alternative names { } + + domain key "/etc/ssl/private/example.tld.key" + domain certificate "/etc/ssl/example.tld_cert-only.crt" + domain full chain certificate "/etc/ssl/example.tld.crt" + sign with letsencrypt +} +``` + +Check the configuration: + +``` +# acme-client -n +``` + +### Configuring the Web server + +Pleroma supports two Web servers: + + * nginx (recommended for most users) + * OpenBSD's httpd and relayd (ONLY for advanced users, media proxy cache is NOT supported and will NOT work properly) + +#### nginx + +Since nginx is not installed by default, install it by running: + +``` +# pkg_add nginx +``` + +Add the following to `/etc/nginx/nginx.conf`, within the `server {}` block listening on port 80 and change `server_name`, as follows: + +``` +http { + ... + + server { + ... + server_name localhost; # Replace with your domain + + location /.well-known/acme-challenge { + rewrite ^/\.well-known/acme-challenge/(.*) /$1 break; + root /var/www/acme; + } + } +} +``` + +Start the nginx service and acquire certificates: + +``` +# rcctl start nginx +# acme-client example.tld +``` + +Add certificate auto-renewal by adding acme-client to `/etc/weekly.local`, replace `example.tld` with your domain: + +``` +# echo "acme-client example.tld && rcctl reload nginx" >> /etc/weekly.local +``` + +OpenBSD's default nginx configuration does not contain an include directive, which is typically used for multiple sites. +Therefore, you will need to first create the required directory as follows: + +``` +# mkdir /etc/nginx/sites-available +# mkdir /etc/nginx/sites-enabled +``` + +Next add the `include` directive to `/etc/nginx/nginx.conf`, within the `http {}` block, as follows: + +``` +http { + ... + + server { + ... + } + + include /etc/nginx/sites-enabled/*; +} +``` + +As root, copy `/home/_pleroma/pleroma/installation/pleroma.nginx` to `/etc/nginx/sites-available/pleroma.nginx`. + +Edit default `/etc/nginx/sites-available/pleroma.nginx` settings and replace `example.tld` with your domain: + + * Uncomment the location block for `~ /\.well-known/acme-challenge` in the server block listening on port 80 + - add `rewrite ^/\.well-known/acme-challenge/(.*) /$1 break;` above the `root` location + - change the `root` location to `/var/www/acme;` + * Change `ssl_trusted_certificate` to `/etc/ssl/example.tld_cert-only.crt` + * Change `ssl_certificate` to `/etc/ssl/example.tld.crt` + * Change `ssl_certificate_key` to `/etc/ssl/private/example.tld.key` + +Remove the following `location {}` block from `/etc/nginx/nginx.conf`, that was previously added for acquiring certificates and change `server_name` back to `localhost`: + +``` +http { + ... + + server { + ... + server_name example.tld; # Change back to localhost + + # Delete this block + location /.well-known/acme-challenge { + rewrite ^/\.well-known/acme-challenge/(.*) /$1 break; + root /var/www/acme; + } + } +} +``` + +Symlink the Pleroma configuration to the enabled sites: + +``` +# ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled +``` + +Check nginx configuration syntax by running: + +``` +# nginx -t +``` + +Note: If the above command complains about a `conflicting server name`, check again that the `location {}` block for acquiring certificates has been removed from `/etc/nginx/nginx.conf` and that the `server_name` has been reverted back to `localhost`. +After doing so run `# nginx -t` again. + +If the configuration is correct, you can now enable and reload the nginx service: + +``` +# rcctl enable nginx +# rcctl reload nginx ``` -To check that it started properly and didn't fail right after starting, you can run `ps aux | grep postgres`, there should be multiple lines of output. #### httpd -httpd will have three functions: + +***Skip this section when using nginx*** + +httpd will have two functions: * redirect requests trying to reach the instance over http to the https URL - * serve a robots.txt file * get Let's Encrypt certificates, with acme-client -Insert the following config in httpd.conf: +As root, copy `/home/_pleroma/pleroma/installation/openbsd/httpd.conf` to `/etc/httpd.conf`, or modify the existing one. + +Edit `/etc/httpd.conf` settings and change: + + * `` with your instance's IPv4 address + * All occurrences of `example.tld` with your instance's domain name + * When using IPv6 also change: + - Uncomment the `ext_inet6=""` line near the beginning of the file and change `* to your server's address(es). If httpd should only listen on one protocol family, comment one of the two first *listen* options. - -Create the /var/www/htdocs/local/ folder and write the content of your robots.txt in /var/www/htdocs/local/robots.txt. -Check the configuration with `httpd -n`, if it is OK enable and start httpd (as root): -``` -rcctl enable httpd -rcctl start httpd +# httpd -n ``` -#### acme-client -acme-client is used to get SSL/TLS certificates from Let's Encrypt. -Insert the following configuration in /etc/acme-client.conf: -``` -# -# $OpenBSD: acme-client.conf,v 1.4 2017/03/22 11:14:14 benno Exp $ -# +If the configuration is correct, enable and start the `httpd` service: -authority letsencrypt- { - #agreement url "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf" - api url "https://acme-v02.api.letsencrypt.org/directory" - account key "/etc/acme/letsencrypt-privkey-.pem" -} +``` +# rcctl enable httpd +# rcctl start httpd +``` -domain { - domain key "/etc/ssl/private/.key" - domain certificate "/etc/ssl/.crt" - domain full chain certificate "/etc/ssl/.fullchain.pem" - sign with letsencrypt- - challengedir "/var/www/acme/" -} -``` -Replace ** by the domain name you'll use for your instance. As root, run `acme-client -n` to check the config, then `acme-client -ADv ` to create account and domain keys, and request a certificate for the first time. -Make acme-client run everyday by adding it in /etc/daily.local. As root, run the following command: `echo "acme-client " >> /etc/daily.local`. +Acquire certificate: -Relayd will look for certificates and keys based on the address it listens on (see next part), the easiest way to make them available to relayd is to create a link, as root run: ``` -ln -s /etc/ssl/.fullchain.pem /etc/ssl/.crt -ln -s /etc/ssl/private/.key /etc/ssl/private/.key +# acme-client example.tld ``` -This will have to be done for each IPv4 and IPv6 address relayd listens on. #### relayd + +***Skip this section when using nginx*** + relayd will be used as the reverse proxy sitting in front of pleroma. -Insert the following configuration in /etc/relayd.conf: + +As root, copy `/home/_pleroma/pleroma/installation/openbsd/relayd.conf` to `/etc/relayd.conf`, or modify the existing one. + +Edit `/etc/relayd.conf` settings and change: + + * `` with your instance's IPv4 address + * All occurrences of `example.tld` with your instance's domain name + * When using IPv6 also change: + - Uncomment the `ext_inet6=""` line near the beginning of the file and change `` to your instance's IPv6 address + - Uncomment the line starting with `listen on $ext_inet6` in the `relay wwwtls` block + +Check the configuration by running: ``` -# $OpenBSD: relayd.conf,v 1.4 2018/03/23 09:55:06 claudio Exp $ - -ext_inet="" -ext_inet6="" - -table { 127.0.0.1 } -table { 127.0.0.1 } - -http protocol plerup { # Protocol for upstream pleroma server - #tcp { nodelay, sack, socket buffer 65536, backlog 128 } # Uncomment and adjust as you see fit - tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305" - tls ecdhe secp384r1 - - # Forward some paths to the local server (as pleroma won't respond to them as you might want) - pass request quick path "/robots.txt" forward to - - # Append a bunch of headers - match request header append "X-Forwarded-For" value "$REMOTE_ADDR" # This two header and the next one are not strictly required by pleroma but adding them won't hurt - match request header append "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT" - - match response header append "X-XSS-Protection" value "1; mode=block" - match response header append "X-Permitted-Cross-Domain-Policies" value "none" - match response header append "X-Frame-Options" value "DENY" - match response header append "X-Content-Type-Options" value "nosniff" - match response header append "Referrer-Policy" value "same-origin" - match response header append "X-Download-Options" value "noopen" - match response header append "Content-Security-Policy" value "default-src 'none'; base-uri 'self'; form-action 'self'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self' wss://CHANGEME.tld; upgrade-insecure-requests;" # Modify "CHANGEME.tld" and set your instance's domain here - match request header append "Connection" value "upgrade" - #match response header append "Strict-Transport-Security" value "max-age=31536000; includeSubDomains" # Uncomment this only after you get HTTPS working. - - # If you do not want remote frontends to be able to access your Pleroma backend server, comment these lines - match response header append "Access-Control-Allow-Origin" value "*" - match response header append "Access-Control-Allow-Methods" value "POST, PUT, DELETE, GET, PATCH, OPTIONS" - match response header append "Access-Control-Allow-Headers" value "Authorization, Content-Type, Idempotency-Key" - match response header append "Access-Control-Expose-Headers" value "Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id" - # Stop commenting lines here -} - -relay wwwtls { - listen on $ext_inet port https tls # Comment to disable listening on IPv4 - listen on $ext_inet6 port https tls # Comment to disable listening on IPv6 - - protocol plerup - - forward to port 4000 check http "/" code 200 - forward to port 80 check http "/robots.txt" code 200 -} -``` -Again, change ** to your server's address(es) and comment one of the two *listen* options if needed. Also change *wss://CHANGEME.tld* to *wss://*. -Check the configuration with `relayd -n`, if it is OK enable and start relayd (as root): -``` -rcctl enable relayd -rcctl start relayd +# relayd -n ``` -##### (Strongly recommended) serve media on another domain +If the configuration is correct, enable and start the `relayd` service: + +``` +# rcctl enable relayd +# rcctl start relayd +``` + +Add certificate auto-renewal by adding acme-client to `/etc/weekly.local`, replace `example.tld` with your domain: + +``` +# echo "acme-client example.tld && rcctl reload relayd" >> /etc/weekly.local +``` + +#### (Strongly recommended) serve media on another domain Refer to the [Hardening your instance](../configuration/hardening.md) document on how to serve media on another domain. We STRONGLY RECOMMEND you to do this to minimize attack vectors. -#### pf -Enabling and configuring pf is highly recommended. -In /etc/pf.conf, insert the following configuration: +### Starting pleroma at boot + +Copy the startup script and make sure it's executable: + ``` -# Macros -if="" -authorized_ssh_clients="any" - -# Skip traffic on loopback interface -set skip on lo - -# Default behavior -set block-policy drop -block in log all -pass out quick - -# Security features -match in all scrub (no-df random-id) -block in log from urpf-failed - -# Rules -pass in quick on $if inet proto icmp to ($if) icmp-type { echoreq unreach paramprob trace } # ICMP -pass in quick on $if inet6 proto icmp6 to ($if) icmp6-type { echoreq unreach paramprob timex toobig } # ICMPv6 -pass in quick on $if proto tcp to ($if) port { http https } # relayd/httpd -pass in quick on $if proto tcp from $authorized_ssh_clients to ($if) port ssh -``` -Replace ** by your server's network interface name (which you can get with ifconfig). Consider replacing the content of the authorized\_ssh\_clients macro by, for example, your home IP address, to avoid SSH connection attempts from bots. - -Check pf's configuration by running `pfctl -nf /etc/pf.conf`, load it with `pfctl -f /etc/pf.conf` and enable pf at boot with `rcctl enable pf`. - -#### Configure and start pleroma -Enter a shell as \_pleroma (as root `su _pleroma -`) and enter pleroma's installation directory (`cd ~/pleroma/`). - -Then follow the main installation guide: - - * run `mix deps.get` - * run `MIX_ENV=prod mix pleroma.instance gen` and enter your instance's information when asked - * copy config/generated\_config.exs to config/prod.secret.exs. The default values should be sufficient but you should edit it and check that everything seems OK. - * exit your current shell back to a root one and run `psql -U postgres -f /home/_pleroma/pleroma/config/setup_db.psql` to setup the database. - * return to a \_pleroma shell into pleroma's installation directory (`su _pleroma -;cd ~/pleroma`) and run `MIX_ENV=prod mix ecto.migrate` - -As \_pleroma in /home/\_pleroma/pleroma, you can now run `LC_ALL=en_US.UTF-8 MIX_ENV=prod mix phx.server` to start your instance. -In another SSH session/tmux window, check that it is working properly by running `ftp -MVo - http://127.0.0.1:4000/api/v1/instance`, you should get json output. Double-check that *uri*'s value is your instance's domain name. - -##### Starting pleroma at boot -An rc script to automatically start pleroma at boot hasn't been written yet, it can be run in a tmux session (tmux is in base). - - -#### Create administrative user - -If your instance is up and running, you can create your first user with administrative rights with the following command as the \_pleroma user. -``` -LC_ALL=en_US.UTF-8 MIX_ENV=prod mix pleroma.user new --admin +# cp /home/_pleroma/pleroma/installation/openbsd/rc.d/pleroma /etc/rc.d/pleroma +# chmod 555 /etc/rc.d/pleroma ``` -#### Further reading +Enable and start the pleroma service: + +``` +# rcctl enable pleroma +# rcctl start pleroma +``` + +### Create administrative user + +If your instance is up and running, you can create your first user with administrative rights with the following commands as the \_pleroma user: + +``` +$ cd pleroma +$ MIX_ENV=prod mix pleroma.user new --admin +``` + +### Further reading {! backend/installation/further_reading.include !} diff --git a/docs/installation/openbsd_fi.md b/docs/installation/openbsd_fi.md index d7c94d8a0..858e64020 100644 --- a/docs/installation/openbsd_fi.md +++ b/docs/installation/openbsd_fi.md @@ -4,7 +4,7 @@ Note: This article is potentially outdated because at this time we may not have Tarvitset: * Oman domainin -* OpenBSD 6.3 -serverin +* OpenBSD 7.5 -serverin * Auttavan ymmärryksen unix-järjestelmistä Komennot, joiden edessä on '#', tulee ajaa käyttäjänä `root`. Tämä on @@ -18,7 +18,7 @@ Matrix-kanava #pleroma:libera.chat ovat hyviä paikkoja löytää apua Asenna tarvittava ohjelmisto: -`# pkg_add git elixir gmake postgresql-server-10.3 postgresql-contrib-10.3 cmake ffmpeg ImageMagick libvips` +`# pkg_add git elixir gmake postgresql-server postgresql-contrib cmake libmagic libvips` #### Optional software diff --git a/installation/freebsd/rc.d/pleroma b/installation/freebsd/rc.d/pleroma index f62aef18d..149b40838 100755 --- a/installation/freebsd/rc.d/pleroma +++ b/installation/freebsd/rc.d/pleroma @@ -24,4 +24,6 @@ command=/usr/local/bin/elixir command_args="--erl \"-detached\" -S /usr/local/bin/mix phx.server" procname="*beam.smp" +PATH="${PATH}:/usr/local/sbin:/usr/local/bin" + run_rc_command "$1" diff --git a/installation/openbsd/httpd.conf b/installation/openbsd/httpd.conf index 82f4803fd..f37325d91 100644 --- a/installation/openbsd/httpd.conf +++ b/installation/openbsd/httpd.conf @@ -2,20 +2,21 @@ # Default httpd.conf file for Pleroma on OpenBSD # Simple installation instructions # 1. Place file in /etc -# 2. Replace with your public IP address -# 3. If using IPv6, uncomment IPv6 lines and replace with your public IPv6 address -# 4. Check file using 'doas httpd -n' -# 5. Enable and start httpd: +# 2. Replace with your public IP address +# 3. If using IPv6, uncomment IPv6 lines and replace with your public IPv6 address +# 4. Replace all occurences of example.tld with your instance's domain name. +# 5. Check file using 'doas httpd -n' +# 6. Enable and start httpd: # # doas rcctl enable httpd # # doas rcctl start httpd # -ext_inet="" -#ext_inet6="" +ext_inet="" +#ext_inet6="" -server "default" { +server "example.tld" { listen on $ext_inet port 80 # Comment to disable listening on IPv4 -# listen on $ext_inet6 port 80 # Comment to disable listening on IPv6 + #listen on $ext_inet6 port 80 # Comment to disable listening on IPv6 listen on 127.0.0.1 port 80 # Do NOT comment this line log syslog @@ -26,10 +27,18 @@ server "default" { request strip 2 } - location "/robots.txt" { root "/htdocs/local/" } - location "/*" { block return 302 "https://$HTTP_HOST$REQUEST_URI" } + location "/*" { block return 301 "https://$HTTP_HOST$REQUEST_URI" } } +# Example of serving a basic static website besides Pleroma using the example configuration in relayd +#server "site.example.tld" { +# listen on 127.0.0.1 port 8080 +# +# location "/*" { +# root "/website" +# } +#} + types { include "/usr/share/misc/mime.types" } diff --git a/installation/openbsd/rc.d/pleromad b/installation/openbsd/rc.d/pleroma similarity index 63% rename from installation/openbsd/rc.d/pleromad rename to installation/openbsd/rc.d/pleroma index 19ac4bb51..6959c20b0 100755 --- a/installation/openbsd/rc.d/pleromad +++ b/installation/openbsd/rc.d/pleroma @@ -4,15 +4,16 @@ # # Simple installation instructions: # 1. Install Pleroma per wiki instructions -# 2. Place this pleromad file in /etc/rc.d +# 2. Place this pleroma file in /etc/rc.d # 3. Enable and start Pleroma -# # doas rcctl enable pleromad -# # doas rcctl start pleromad +# # doas rcctl enable pleroma +# # doas rcctl start pleroma # daemon="/usr/local/bin/elixir" -daemon_flags="--detached -S /usr/local/bin/mix phx.server" +daemon_flags="--erl \"-detached\" -S /usr/local/bin/mix phx.server" daemon_user="_pleroma" +daemon_execdir="/home/_pleroma/pleroma" . /etc/rc.d/rc.subr @@ -23,10 +24,6 @@ rc_check() { pgrep -q -U _pleroma -f "phx.server" } -rc_start() { - ${rcexec} "cd pleroma; ${daemon} ${daemon_flags}" -} - rc_stop() { pkill -q -U _pleroma -f "phx.server" } diff --git a/installation/openbsd/relayd.conf b/installation/openbsd/relayd.conf index 31c2c1129..8b7be4ca6 100644 --- a/installation/openbsd/relayd.conf +++ b/installation/openbsd/relayd.conf @@ -3,9 +3,10 @@ # Simple installation instructions: # 1. Place in /etc # 2. Replace with your public IPv4 address -# 3. If using IPv6i, uncomment IPv6 lines and replace with your public IPv6 address -# 4. Check file using 'doas relayd -n' -# 5. Reload/start relayd +# 3. If using IPv6, uncomment IPv6 lines and replace with your public IPv6 address +# 4. Replace all occurrences of example.tld with your instance's domain +# 5. Check file using 'doas relayd -n' +# 6. Reload/start relayd # # doas rcctl enable relayd # # doas rcctl start relayd # @@ -14,31 +15,54 @@ ext_inet="" #ext_inet6="" table { 127.0.0.1 } -table { 127.0.0.1 } -http protocol plerup { # Protocol for upstream pleroma server +# Uncomment when you want to serve other services than Pleroma. +# In this example tables are used only as way to differentiate between Pleroma and other services. +# Feel free to rename "httpd_server" everywhere to fit your setup. +#table { 127.0.0.1 } + +http protocol pleroma { # Protocol for upstream Pleroma server #tcp { nodelay, sack, socket buffer 65536, backlog 128 } # Uncomment and adjust as you see fit - tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA0-POLY1305" - tls ecdhe secp384r1 + tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" + tls ecdhe "X25519,P-256,P-384,secp521r1" # relayd default+secp521r1 - # Forward some paths to the local server (as pleroma won't respond to them as you might want) - pass request quick path "/robots.txt" forward to + return error - # Append a bunch of headers - match request header append "X-Forwarded-For" value "$REMOTE_ADDR" # This two header and the next one are not strictl required by pleroma but adding them won't hurt - match request header append "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT" + # When serving multiple services with different certificates, specify multiple "tls keypair" keywords + # and add forwards to those services before the block keyword near the bottom of the protocol and relay configurations. + # The string in quotes must match the fullchain certificate file created by acme-client without the extension. + # For example: + # tls keypair "pleroma.example.tld" + # tls keypair "example.tld" + tls keypair "example.tld" + match request header append "X-Forwarded-For" value "$REMOTE_ADDR" match request header append "Connection" value "upgrade" + # When hosting Pleroma on a subdomain, replace example.tld accordingly (not the base domain). + # From the above example, "example.tld" should be replaced with "pleroma.example.tld" instead. + pass request quick header "Host" value "example.tld" forward to + + # Uncomment when serving media uploads on a different (sub)domain. + # Keep media proxy disabled, as it will NOT work under relayd/httpd. If you want to also setup media proxy, use nginx instead. + #pass request quick header "Host" value "media.example.tld" forward to + + # When serving multiple services, add the forwards here. + # Example: + #pass request quick header "Host" value "example.tld" forward to + + block } relay wwwtls { listen on $ext_inet port https tls # Comment to disable listening on IPv4 -# listen on $ext_inet6 port https tls # Comment to disable listening on IPv6 + #listen on $ext_inet6 port https tls # Comment to disable listening on IPv6 - protocol plerup + protocol pleroma - forward to port 4000 check http "/" code 200 - forward to port 80 check http "/robots.txt" code 200 + forward to port 4000 check tcp timeout 500 # Adjust timeout accordingly when relayd returns 502 while Pleroma is running without problems. + + # When serving multiple services, add the forwards here. + # Example: + #forward to port 8080 } - diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 0dc30549c..143af5cdd 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -271,7 +271,7 @@ defmodule Mix.Tasks.Pleroma.Instance do [config_dir, psql_dir, static_dir, uploads_dir] |> Enum.reject(&File.exists?/1) |> Enum.each(fn dir -> - File.mkdir_p!(dir) + Pleroma.Backports.mkdir_p!(dir) File.chmod!(dir, 0o700) end) diff --git a/lib/mix/tasks/pleroma/robots_txt.ex b/lib/mix/tasks/pleroma/robots_txt.ex index 5124c7c40..e741f3cf0 100644 --- a/lib/mix/tasks/pleroma/robots_txt.ex +++ b/lib/mix/tasks/pleroma/robots_txt.ex @@ -22,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.RobotsTxt do static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/") if !File.exists?(static_dir) do - File.mkdir_p!(static_dir) + Pleroma.Backports.mkdir_p!(static_dir) end robots_txt_path = Path.join(static_dir, "robots.txt") diff --git a/lib/mix/tasks/pleroma/test_runner.ex b/lib/mix/tasks/pleroma/test_runner.ex index 69fefb001..67820247e 100644 --- a/lib/mix/tasks/pleroma/test_runner.ex +++ b/lib/mix/tasks/pleroma/test_runner.ex @@ -4,7 +4,9 @@ defmodule Mix.Tasks.Pleroma.TestRunner do use Mix.Task def run(args \\ []) do - case System.cmd("mix", ["test"] ++ args, into: IO.stream(:stdio, :line)) do + case System.cmd("mix", ["test", "--warnings-as-errors"] ++ args, + into: IO.stream(:stdio, :line) + ) do {_, 0} -> :ok diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 3f199c002..8e1c5de0d 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -43,9 +43,6 @@ defmodule Pleroma.Application do # every time the application is restarted, so we disable module # conflicts at runtime Code.compiler_options(ignore_module_conflict: true) - # Disable warnings_as_errors at runtime, it breaks Phoenix live reload - # due to protocol consolidation warnings - Code.compiler_options(warnings_as_errors: false) Pleroma.Telemetry.Logger.attach() Config.Holder.save_default() Pleroma.HTML.compile_scrubbers() @@ -56,7 +53,10 @@ defmodule Pleroma.Application do Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled() end - Pleroma.ApplicationRequirements.verify!() + if Config.get(:env) != :test do + Pleroma.ApplicationRequirements.verify!() + end + load_custom_modules() Pleroma.Docs.JSON.compile() limiters_setup() @@ -68,26 +68,11 @@ defmodule Pleroma.Application do Finch.start_link(name: MyFinch) end - if adapter == Tesla.Adapter.Gun do - if version = Pleroma.OTPVersion.version() do - [major, minor] = - version - |> String.split(".") - |> Enum.map(&String.to_integer/1) - |> Enum.take(2) - - if (major == 22 and minor < 2) or major < 22 do - raise " - !!!OTP VERSION WARNING!!! - You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2. - " - end - else - raise " - !!!OTP VERSION WARNING!!! - To support correct handling of unordered certificates chains - OTP version must be > 22.2. - " - end + # Disable warnings_as_errors at runtime, it breaks Phoenix live reload + # due to protocol consolidation warnings + # :warnings_as_errors is deprecated via Code.compiler_options/2 since 1.18 + if Version.compare(System.version(), "1.18.0") == :lt do + Code.compiler_options(warnings_as_errors: false) end # Define workers and child supervisors to be supervised @@ -169,7 +154,8 @@ defmodule Pleroma.Application do limit: 500_000 ), build_cachex("rel_me", limit: 2500), - build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5000) + build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5_000), + build_cachex("translations", default_ttl: :timer.hours(24), limit: 5_000) ] end diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex index a334d12ee..87ecb7e2d 100644 --- a/lib/pleroma/application_requirements.ex +++ b/lib/pleroma/application_requirements.ex @@ -189,7 +189,40 @@ defmodule Pleroma.ApplicationRequirements do false end - if Enum.all?([preview_proxy_commands_status | filter_commands_statuses], & &1) do + language_detector_commands_status = + if Pleroma.Language.LanguageDetector.missing_dependencies() == [] do + true + else + Logger.error( + "The following dependencies required by the currently enabled " <> + "language detection provider are not installed: " <> + inspect(Pleroma.Language.LanguageDetector.missing_dependencies()) + ) + + false + end + + translation_commands_status = + if Pleroma.Language.Translation.missing_dependencies() == [] do + true + else + Logger.error( + "The following dependencies required by the currently enabled " <> + "translation provider are not installed: " <> + inspect(Pleroma.Language.Translation.missing_dependencies()) + ) + + false + end + + if Enum.all?( + [ + preview_proxy_commands_status, + language_detector_commands_status, + translation_commands_status | filter_commands_statuses + ], + & &1 + ) do :ok else {:error, diff --git a/lib/pleroma/backports.ex b/lib/pleroma/backports.ex new file mode 100644 index 000000000..68cb7b990 --- /dev/null +++ b/lib/pleroma/backports.ex @@ -0,0 +1,72 @@ +# Copyright 2012 Plataformatec +# Copyright 2021 The Elixir Team +# SPDX-License-Identifier: Apache-2.0 + +defmodule Pleroma.Backports do + import File, only: [dir?: 1] + + # + # To be removed when we require Elixir 1.19 + @doc """ + Tries to create the directory `path`. + + Missing parent directories are created. Returns `:ok` if successful, or + `{:error, reason}` if an error occurs. + + Typical error reasons are: + + * `:eacces` - missing search or write permissions for the parent + directories of `path` + * `:enospc` - there is no space left on the device + * `:enotdir` - a component of `path` is not a directory + + """ + @spec mkdir_p(Path.t()) :: :ok | {:error, File.posix() | :badarg} + def mkdir_p(path) do + do_mkdir_p(IO.chardata_to_string(path)) + end + + defp do_mkdir_p("/") do + :ok + end + + defp do_mkdir_p(path) do + parent = Path.dirname(path) + + if parent == path do + :ok + else + case do_mkdir_p(parent) do + :ok -> + case :file.make_dir(path) do + {:error, :eexist} -> + if dir?(path), do: :ok, else: {:error, :enotdir} + + other -> + other + end + + e -> + e + end + end + end + + @doc """ + Same as `mkdir_p/1`, but raises a `File.Error` exception in case of failure. + Otherwise `:ok`. + """ + @spec mkdir_p!(Path.t()) :: :ok + def mkdir_p!(path) do + case mkdir_p(path) do + :ok -> + :ok + + {:error, reason} -> + raise File.Error, + reason: reason, + action: "make directory (with -p)", + path: IO.chardata_to_string(path) + end + end +end diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex index 89d3050d6..e9990fa35 100644 --- a/lib/pleroma/config_db.ex +++ b/lib/pleroma/config_db.ex @@ -302,7 +302,7 @@ defmodule Pleroma.ConfigDB do end def to_elixir_types(%{"tuple" => entity}) do - Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1))) + Enum.reduce(entity, {}, &Tuple.insert_at(&2, tuple_size(&2), to_elixir_types(&1))) end def to_elixir_types(entity) when is_map(entity) do diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex index 2cb8112ec..5ec1101b2 100644 --- a/lib/pleroma/constants.ex +++ b/lib/pleroma/constants.ex @@ -100,6 +100,7 @@ defmodule Pleroma.Constants do "Add", "Remove", "Like", + "Dislike", "Announce", "Undo", "Flag", @@ -115,6 +116,7 @@ defmodule Pleroma.Constants do "Flag", "Follow", "Like", + "Dislike", "EmojiReact", "Announce" ] diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index c58748d3c..99fa1994f 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -488,7 +488,7 @@ defmodule Pleroma.Emoji.Pack do with true <- String.contains?(file_path, "/"), path <- Path.dirname(file_path), false <- File.exists?(path) do - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) end end @@ -536,7 +536,7 @@ defmodule Pleroma.Emoji.Pack do emoji_path = emoji_path() # Create the directory first if it does not exist. This is probably the first request made # with the API so it should be sufficient - with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)}, + with {:create_dir, :ok} <- {:create_dir, Pleroma.Backports.mkdir_p(emoji_path)}, {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do {:ok, Enum.sort(results)} else @@ -561,7 +561,7 @@ defmodule Pleroma.Emoji.Pack do end defp unzip(archive, pack_info, remote_pack, local_pack) do - with :ok <- File.mkdir_p!(local_pack.path) do + with :ok <- Pleroma.Backports.mkdir_p!(local_pack.path) do files = Enum.map(remote_pack["files"], fn {_, path} -> path end) # Fallback cannot contain a pack.json file files = if pack_info[:fallback], do: files, else: ["pack.json" | files] diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index fe7f525ea..e651d7d9d 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -66,7 +66,7 @@ defmodule Pleroma.Frontend do def unzip(zip, dest) do File.rm_rf!(dest) - File.mkdir_p!(dest) + Pleroma.Backports.mkdir_p!(dest) case Pleroma.SafeZip.unzip_data(zip, dest) do {:ok, _} -> :ok @@ -90,7 +90,7 @@ defmodule Pleroma.Frontend do defp install_frontend(frontend_info, source, dest) do from = frontend_info["build_dir"] || "dist" File.rm_rf!(dest) - File.mkdir_p!(dest) + Pleroma.Backports.mkdir_p!(dest) File.cp_r!(Path.join([source, from]), dest) :ok end diff --git a/lib/pleroma/language/language_detector.ex b/lib/pleroma/language/language_detector.ex new file mode 100644 index 000000000..68d243562 --- /dev/null +++ b/lib/pleroma/language/language_detector.ex @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.LanguageDetector do + import Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode, + only: [good_locale_code?: 1] + + @words_threshold 4 + @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) + + def configured? do + provider = get_provider() + + !!provider and provider.configured?() + end + + def missing_dependencies do + provider = get_provider() + + if provider do + provider.missing_dependencies() + else + [] + end + end + + # Strip tags from text, etc. + defp prepare_text(text) do + text + |> Floki.parse_fragment!() + |> Floki.filter_out( + ".h-card, .mention, .hashtag, .u-url, .quote-inline, .recipients-inline, code, pre" + ) + |> Floki.text() + end + + def detect(text) do + provider = get_provider() + + text = prepare_text(text) + word_count = text |> String.split(~r/\s+/) |> Enum.count() + + if word_count < @words_threshold or !provider or !provider.configured?() do + nil + else + with language <- provider.detect(text), + true <- good_locale_code?(language) do + language + else + _ -> nil + end + end + end + + defp get_provider do + @config_impl.get([__MODULE__, :provider]) + end +end diff --git a/lib/pleroma/language/language_detector/fasttext.ex b/lib/pleroma/language/language_detector/fasttext.ex new file mode 100644 index 000000000..0f621a000 --- /dev/null +++ b/lib/pleroma/language/language_detector/fasttext.ex @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.LanguageDetector.Fasttext do + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] + + alias Pleroma.Language.LanguageDetector.Provider + + @behaviour Provider + + @impl Provider + def missing_dependencies do + if Pleroma.Utils.command_available?("fasttext") do + [] + else + ["fasttext"] + end + end + + @impl Provider + def configured?, do: not_empty_string(get_model()) + + @impl Provider + def detect(text) do + text_path = Path.join(System.tmp_dir!(), "fasttext-#{Ecto.UUID.generate()}") + + File.write(text_path, text |> String.replace(~r/\s+/, " ")) + + detected_language = + case System.cmd("fasttext", ["predict", get_model(), text_path]) do + {"__label__" <> language, _} -> + language |> String.trim() + + _ -> + nil + end + + File.rm(text_path) + + detected_language + end + + defp get_model do + Pleroma.Config.get([__MODULE__, :model]) + end +end diff --git a/lib/pleroma/language/language_detector/provider.ex b/lib/pleroma/language/language_detector/provider.ex new file mode 100644 index 000000000..08e7c8eef --- /dev/null +++ b/lib/pleroma/language/language_detector/provider.ex @@ -0,0 +1,11 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.LanguageDetector.Provider do + @callback missing_dependencies() :: [String.t()] + + @callback configured?() :: boolean() + + @callback detect(text :: String.t()) :: String.t() | nil +end diff --git a/lib/pleroma/language/translation.ex b/lib/pleroma/language/translation.ex new file mode 100644 index 000000000..64f115ed8 --- /dev/null +++ b/lib/pleroma/language/translation.ex @@ -0,0 +1,127 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.Translation do + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + + def configured? do + provider = get_provider() + + !!provider and provider.configured?() + end + + def missing_dependencies do + provider = get_provider() + + if provider do + provider.missing_dependencies() + else + [] + end + end + + def translate(text, source_language, target_language) do + cache_key = get_cache_key(text, source_language, target_language) + + case @cachex.get(:translations_cache, cache_key) do + {:ok, nil} -> + provider = get_provider() + + result = + if !configured?() do + {:error, :not_found} + else + provider.translate(text, source_language, target_language) + |> scrub_html() + end + + store_result(result, cache_key) + + result + + {:ok, result} -> + {:ok, result} + + {:error, error} -> + {:error, error} + end + end + + def supported_languages(type) when type in [:source, :target] do + provider = get_provider() + + cache_key = "#{type}_languages/#{provider.name()}" + + case @cachex.get(:translations_cache, cache_key) do + {:ok, nil} -> + result = + if !configured?() do + {:error, :not_found} + else + provider.supported_languages(type) + end + + store_result(result, cache_key) + + result + + {:ok, result} -> + {:ok, result} + + {:error, error} -> + {:error, error} + end + end + + def languages_matrix do + provider = get_provider() + + cache_key = "languages_matrix/#{provider.name()}" + + case @cachex.get(:translations_cache, cache_key) do + {:ok, nil} -> + result = + if !configured?() do + {:error, :not_found} + else + provider.languages_matrix() + end + + store_result(result, cache_key) + + result + + {:ok, result} -> + {:ok, result} + + {:error, error} -> + {:error, error} + end + end + + defp get_provider, do: Pleroma.Config.get([__MODULE__, :provider]) + + defp get_cache_key(text, source_language, target_language) do + "#{source_language}/#{target_language}/#{content_hash(text)}" + end + + defp store_result({:ok, result}, cache_key) do + @cachex.put(:translations_cache, cache_key, result) + end + + defp store_result(_, _), do: nil + + defp content_hash(text), do: :crypto.hash(:sha256, text) |> Base.encode64() + + defp scrub_html({:ok, %{content: content} = result}) when is_binary(content) do + scrubbers = Pleroma.Config.get([:markup, :scrub_policy]) + + content + |> Pleroma.HTML.filter_tags(scrubbers) + + {:ok, %{result | content: content}} + end + + defp scrub_html(result), do: result +end diff --git a/lib/pleroma/language/translation/deepl.ex b/lib/pleroma/language/translation/deepl.ex new file mode 100644 index 000000000..aaaac9b0f --- /dev/null +++ b/lib/pleroma/language/translation/deepl.ex @@ -0,0 +1,121 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.Translation.Deepl do + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] + + alias Pleroma.Language.Translation.Provider + + use Provider + + @behaviour Provider + + @name "DeepL" + + @impl Provider + def configured?, do: not_empty_string(base_url()) and not_empty_string(api_key()) + + @impl Provider + def translate(content, source_language, target_language) do + endpoint = + base_url() + |> URI.merge("/v2/translate") + |> URI.to_string() + + case Pleroma.HTTP.post( + endpoint, + Jason.encode!(%{ + text: [content], + source_lang: source_language |> String.upcase(), + target_lang: target_language, + tag_handling: "html" + }), + [ + {"Content-Type", "application/json"}, + {"Authorization", "DeepL-Auth-Key #{api_key()}"} + ] + ) do + {:ok, %{status: 429}} -> + {:error, :too_many_requests} + + {:ok, %{status: 456}} -> + {:error, :quota_exceeded} + + {:ok, %{status: 200} = res} -> + %{ + "translations" => [ + %{"text" => content, "detected_source_language" => detected_source_language} + ] + } = Jason.decode!(res.body) + + {:ok, + %{ + content: content, + detected_source_language: detected_source_language, + provider: @name + }} + + _ -> + {:error, :internal_server_error} + end + end + + @impl Provider + def supported_languages(type) when type in [:source, :target] do + endpoint = + base_url() + |> URI.merge("/v2/languages") + |> URI.to_string() + + case Pleroma.HTTP.post( + endpoint <> "?" <> URI.encode_query(%{type: type}), + "", + [ + {"Content-Type", "application/x-www-form-urlencoded"}, + {"Authorization", "DeepL-Auth-Key #{api_key()}"} + ] + ) do + {:ok, %{status: 200} = res} -> + languages = + Jason.decode!(res.body) + |> Enum.map(fn %{"language" => language} -> language |> String.downcase() end) + |> Enum.map(fn language -> + if String.contains?(language, "-") do + [language, language |> String.split("-") |> Enum.at(0)] + else + language + end + end) + |> List.flatten() + |> Enum.uniq() + + {:ok, languages} + + _ -> + {:error, :internal_server_error} + end + end + + @impl Provider + def languages_matrix do + with {:ok, source_languages} <- supported_languages(:source), + {:ok, target_languages} <- supported_languages(:target) do + {:ok, + Map.new(source_languages, fn language -> {language, target_languages -- [language]} end)} + else + {:error, error} -> {:error, error} + end + end + + @impl Provider + def name, do: @name + + defp base_url do + Pleroma.Config.get([__MODULE__, :base_url]) + end + + defp api_key do + Pleroma.Config.get([__MODULE__, :api_key]) + end +end diff --git a/lib/pleroma/language/translation/libretranslate.ex b/lib/pleroma/language/translation/libretranslate.ex new file mode 100644 index 000000000..fd727d1cf --- /dev/null +++ b/lib/pleroma/language/translation/libretranslate.ex @@ -0,0 +1,93 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.Translation.Libretranslate do + import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1] + + alias Pleroma.Language.Translation.Provider + + use Provider + + @behaviour Provider + + @name "LibreTranslate" + + @impl Provider + def configured?, do: not_empty_string(base_url()) and not_empty_string(api_key()) + + @impl Provider + def translate(content, source_language, target_language) do + case Pleroma.HTTP.post( + base_url() <> "/translate", + Jason.encode!(%{ + q: content, + source: source_language |> String.upcase(), + target: target_language, + format: "html", + api_key: api_key() + }), + [ + {"Content-Type", "application/json"} + ] + ) do + {:ok, %{status: 429}} -> + {:error, :too_many_requests} + + {:ok, %{status: 403}} -> + {:error, :quota_exceeded} + + {:ok, %{status: 200} = res} -> + %{ + "translatedText" => content + } = Jason.decode!(res.body) + + {:ok, + %{ + content: content, + detected_source_language: source_language, + provider: @name + }} + + _ -> + {:error, :internal_server_error} + end + end + + @impl Provider + def supported_languages(_) do + case Pleroma.HTTP.get(base_url() <> "/languages") do + {:ok, %{status: 200} = res} -> + languages = + Jason.decode!(res.body) + |> Enum.map(fn %{"code" => code} -> code end) + + {:ok, languages} + + _ -> + {:error, :internal_server_error} + end + end + + @impl Provider + def languages_matrix do + with {:ok, source_languages} <- supported_languages(:source), + {:ok, target_languages} <- supported_languages(:target) do + {:ok, + Map.new(source_languages, fn language -> {language, target_languages -- [language]} end)} + else + {:error, error} -> {:error, error} + end + end + + @impl Provider + def name, do: @name + + defp base_url do + Pleroma.Config.get([__MODULE__, :base_url]) + end + + defp api_key do + Pleroma.Config.get([__MODULE__, :api_key], "") + end +end diff --git a/lib/pleroma/language/translation/provider.ex b/lib/pleroma/language/translation/provider.ex new file mode 100644 index 000000000..533b5355a --- /dev/null +++ b/lib/pleroma/language/translation/provider.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.Translation.Provider do + alias Pleroma.Language.Translation.Provider + + @callback missing_dependencies() :: [String.t()] + + @callback configured?() :: boolean() + + @callback translate( + content :: String.t(), + source_language :: String.t(), + target_language :: String.t() + ) :: + {:ok, + %{ + content: String.t(), + detected_source_language: String.t(), + provider: String.t() + }} + | {:error, atom()} + + @callback supported_languages(type :: :string | :target) :: + {:ok, [String.t()]} | {:error, atom()} + + @callback languages_matrix() :: {:ok, Map.t()} | {:error, atom()} + + @callback name() :: String.t() + + defmacro __using__(_opts) do + quote do + @impl Provider + def missing_dependencies, do: [] + + defoverridable missing_dependencies: 0 + end + end +end diff --git a/lib/pleroma/otp_version.ex b/lib/pleroma/otp_version.ex deleted file mode 100644 index 80b15275a..000000000 --- a/lib/pleroma/otp_version.ex +++ /dev/null @@ -1,28 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.OTPVersion do - @spec version() :: String.t() | nil - def version do - # OTP Version https://erlang.org/doc/system_principles/versions.html#otp-version - [ - Path.join(:code.root_dir(), "OTP_VERSION"), - Path.join([:code.root_dir(), "releases", :erlang.system_info(:otp_release), "OTP_VERSION"]) - ] - |> get_version_from_files() - end - - @spec get_version_from_files([Path.t()]) :: String.t() | nil - def get_version_from_files([]), do: nil - - def get_version_from_files([path | paths]) do - if File.exists?(path) do - path - |> File.read!() - |> String.replace(~r/\r|\n|\s/, "") - else - get_version_from_files(paths) - end - end -end diff --git a/lib/pleroma/search/database_search.ex b/lib/pleroma/search/database_search.ex index aef5d1e74..e88d632cb 100644 --- a/lib/pleroma/search/database_search.ex +++ b/lib/pleroma/search/database_search.ex @@ -102,7 +102,8 @@ defmodule Pleroma.Search.DatabaseSearch do ^tsc, o.data, ^search_query - ) + ), + order_by: [desc: :inserted_at] ) end diff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex index e4a309cea..7aab05b36 100644 --- a/lib/pleroma/uploaders/local.ex +++ b/lib/pleroma/uploaders/local.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Uploaders.Local do [file | folders] -> path = Path.join([upload_path()] ++ Enum.reverse(folders)) - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) {path, file} end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index d9da9ede1..84551afd5 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -150,7 +150,7 @@ defmodule Pleroma.User do field(:allow_following_move, :boolean, default: true) field(:skip_thread_containment, :boolean, default: false) field(:actor_type, :string, default: "Person") - field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: []) + field(:also_known_as, {:array, ObjectValidators.BareUri}, default: []) field(:inbox, :string) field(:shared_inbox, :string) field(:accepts_chat_messages, :boolean, default: nil) @@ -308,7 +308,7 @@ defmodule Pleroma.User do def binary_id(%User{} = user), do: binary_id(user.id) - @doc "Returns status account" + @doc "Returns account status" @spec account_status(User.t()) :: account_status() def account_status(%User{is_active: false}), do: :deactivated def account_status(%User{password_reset_pending: true}), do: :password_reset_pending @@ -895,7 +895,7 @@ defmodule Pleroma.User do end) end - def validate_email_not_in_blacklisted_domain(changeset, field) do + defp validate_email_not_in_blacklisted_domain(changeset, field) do validate_change(changeset, field, fn _, value -> valid? = Config.get([User, :email_blacklist]) @@ -912,9 +912,9 @@ defmodule Pleroma.User do end) end - def maybe_validate_required_email(changeset, true), do: changeset + defp maybe_validate_required_email(changeset, true), do: changeset - def maybe_validate_required_email(changeset, _) do + defp maybe_validate_required_email(changeset, _) do if Config.get([:instance, :account_activation_required]) do validate_required(changeset, [:email]) else @@ -1109,15 +1109,15 @@ defmodule Pleroma.User do defp maybe_send_registration_email(_), do: {:ok, :noop} - def needs_update?(%User{local: true}), do: false + defp needs_update?(%User{local: true}), do: false - def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true + defp needs_update?(%User{local: false, last_refreshed_at: nil}), do: true - def needs_update?(%User{local: false} = user) do + defp needs_update?(%User{local: false} = user) do NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400 end - def needs_update?(_), do: true + defp needs_update?(_), do: true @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()} @@ -1708,7 +1708,9 @@ defmodule Pleroma.User do end end - def block(%User{} = blocker, %User{} = blocked) do + def block(blocker, blocked, params \\ %{}) + + def block(%User{} = blocker, %User{} = blocked, params) do # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213) blocker = if following?(blocker, blocked) do @@ -1738,12 +1740,33 @@ defmodule Pleroma.User do {:ok, blocker} = update_follower_count(blocker) {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked) - add_to_block(blocker, blocked) + + duration = Map.get(params, :duration, 0) + + expires_at = + if duration > 0 do + DateTime.utc_now() + |> DateTime.add(duration) + else + nil + end + + user_block = add_to_block(blocker, blocked, expires_at) + + if duration > 0 do + Pleroma.Workers.MuteExpireWorker.new( + %{"op" => "unblock_user", "blocker_id" => blocker.id, "blocked_id" => blocked.id}, + scheduled_at: expires_at + ) + |> Oban.insert() + end + + user_block end # helper to handle the block given only an actor's AP id - def block(%User{} = blocker, %{ap_id: ap_id}) do - block(blocker, get_cached_by_ap_id(ap_id)) + def block(%User{} = blocker, %{ap_id: ap_id}, params) do + block(blocker, get_cached_by_ap_id(ap_id), params) end def unblock(%User{} = blocker, %User{} = blocked) do @@ -1984,7 +2007,7 @@ defmodule Pleroma.User do end @spec purge_user_changeset(User.t()) :: Ecto.Changeset.t() - def purge_user_changeset(user) do + defp purge_user_changeset(user) do # "Right to be forgotten" # https://gdpr.eu/right-to-be-forgotten/ change(user, %{ @@ -2156,7 +2179,7 @@ defmodule Pleroma.User do Repo.all(query) end - def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do + defp delete_notifications_from_user_activities(%User{ap_id: ap_id}) do Notification |> join(:inner, [n], activity in assoc(n, :activity)) |> where([n, a], fragment("? = ?", a.actor, ^ap_id)) @@ -2615,7 +2638,7 @@ defmodule Pleroma.User do end end - # Internal function; public one is `deactivate/2` + # Internal function; public one is `set_activation/2` defp set_activation_status(user, status) do user |> cast(%{is_active: status}, [:is_active]) @@ -2634,7 +2657,7 @@ defmodule Pleroma.User do |> update_and_set_cache() end - def validate_fields(changeset, remote? \\ false) do + defp validate_fields(changeset, remote?) do limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields limit = Config.get([:instance, limit_name], 0) @@ -2779,10 +2802,10 @@ defmodule Pleroma.User do set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked)) end - @spec add_to_block(User.t(), User.t()) :: + @spec add_to_block(User.t(), User.t(), integer() | nil) :: {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()} - defp add_to_block(%User{} = user, %User{} = blocked) do - with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do + defp add_to_block(%User{} = user, %User{} = blocked, expires_at) do + with {:ok, relationship} <- UserRelationship.create_block(user, blocked, expires_at) do @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}") {:ok, relationship} end diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 244b08adb..3f67cdf0c 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -193,7 +193,7 @@ defmodule Pleroma.User.Backup do backup = Repo.preload(backup, :user) tempfile = Path.join([backup.tempdir, backup.file_name]) - with {_, :ok} <- {:mkdir, File.mkdir_p(backup.tempdir)}, + with {_, :ok} <- {:mkdir, Pleroma.Backports.mkdir_p(backup.tempdir)}, {_, :ok} <- {:actor, actor(backup.tempdir, backup.user)}, {_, :ok} <- {:statuses, statuses(backup.tempdir, backup.user)}, {_, :ok} <- {:likes, likes(backup.tempdir, backup.user)}, diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 2a1e56278..046316024 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -327,8 +327,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do }, []} end - @spec block(User.t(), User.t()) :: {:ok, map(), keyword()} - def block(blocker, blocked) do + @spec block(User.t(), User.t(), map()) :: {:ok, map(), keyword()} + def block(blocker, blocked, params \\ %{}) do {:ok, %{ "id" => Utils.generate_activity_id(), @@ -336,7 +336,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do "actor" => blocker.ap_id, "object" => blocked.ap_id, "to" => [blocked.ap_id] - }, []} + }, Keyword.new(params)} end @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()} diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex index 49d17d8b9..54f0e6bc1 100644 --- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex @@ -87,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do Path.join(Config.get([:instance, :static_dir]), "emoji/stolen") ) - File.mkdir_p(emoji_dir_path) + Pleroma.Backports.mkdir_p(emoji_dir_path) new_emojis = foreign_emojis diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index ee12f3ebf..17652a0de 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -200,14 +200,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do end def validate(%{"type" => type} = object, meta) - when type in ~w[Accept Reject Follow Update Like EmojiReact Announce + when type in ~w[Accept Reject Follow Like EmojiReact Announce ChatMessage Answer] do validator = case type do "Accept" -> AcceptRejectValidator "Reject" -> AcceptRejectValidator "Follow" -> FollowValidator - "Update" -> UpdateValidator "Like" -> LikeValidator "EmojiReact" -> EmojiReactValidator "Announce" -> AnnounceValidator @@ -215,16 +214,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do "Answer" -> AnswerValidator end - cast_func = - if type == "Update" do - fn o -> validator.cast_and_validate(o, meta) end - else - fn o -> validator.cast_and_validate(o) end - end - with {:ok, object} <- object - |> cast_func.() + |> validator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + + def validate(%{"type" => type} = object, meta) when type == "Update" do + with {:ok, object} <- + object + |> UpdateValidator.cast_and_validate(meta) |> Ecto.Changeset.apply_action(:insert) do object = stringify_keys(object) {:ok, object, meta} diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex index 87d3e0c8f..f0f3fef90 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do alias Pleroma.EctoType.ActivityPub.ObjectValidators + alias Pleroma.Language.LanguageDetector alias Pleroma.Maps alias Pleroma.Object alias Pleroma.Object.Containment @@ -151,10 +152,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do def maybe_add_language(object) do language = [ - get_language_from_context(object), - get_language_from_content_map(object) + &get_language_from_context/1, + &get_language_from_content_map/1, + &get_language_from_content/1 ] - |> Enum.find(&good_locale_code?(&1)) + |> Enum.find_value(fn get_language -> + language = get_language.(object) + + if good_locale_code?(language) do + language + else + nil + end + end) if language do Map.put(object, "language", language) @@ -187,6 +197,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do defp get_language_from_content_map(_), do: nil + defp get_language_from_content(%{"content" => content} = object) do + LanguageDetector.detect("#{object["summary"] || ""} #{content}") + end + + defp get_language_from_content(_), do: nil + def maybe_add_content_map(%{"language" => language, "content" => content} = object) when not_empty_string(language) do Map.put(object, "contentMap", Map.put(%{}, language, content)) diff --git a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex index 47cf7b415..dc2770189 100644 --- a/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/tag_validator.ex @@ -50,13 +50,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do end def changeset(struct, %{"type" => "Hashtag", "name" => name} = data) do - name = - cond do - "#" <> name -> name - name -> name - end - |> String.downcase() - + name = String.downcase(name) data = Map.put(data, "name", name) struct diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 0de3a0d43..f160f1e17 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -93,7 +93,20 @@ defmodule Pleroma.Web.ActivityPub.Publisher do {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - cc = Map.get(params, :cc, []) + param_cc = Map.get(params, :cc, []) + + original_cc = Map.get(data, "cc", []) + + public_address = Pleroma.Constants.as_public() + + # Ensure unlisted posts don't lose the public address in the cc + # if the param_cc was set + cc = + if public_address in original_cc and public_address not in param_cc do + [public_address | param_cc] + else + param_cc + end json = data diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index d6d403671..52cdc3c3f 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -145,7 +145,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do ) do with %User{} = blocker <- User.get_cached_by_ap_id(blocking_user), %User{} = blocked <- User.get_cached_by_ap_id(blocked_user) do - User.block(blocker, blocked) + User.block(blocker, blocked, Enum.into(meta, %{})) end {:ok, object, meta} diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 1e6ee7dc8..8819e1596 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -492,6 +492,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do } # Rewrite misskey likes into EmojiReacts + defp handle_incoming_normalized( + %{ + "type" => "Like", + "content" => content + } = data, + options + ) + when is_binary(content) do + data + |> Map.put("type", "EmojiReact") + |> handle_incoming_normalized(options) + end + defp handle_incoming_normalized( %{ "type" => "Like", @@ -500,7 +513,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do options ) do data - |> Map.put("type", "EmojiReact") |> Map.put("content", @misskey_reactions[reaction] || reaction) |> handle_incoming_normalized(options) end @@ -652,6 +664,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end + # Rewrite dislikes into the thumbs down emoji + defp handle_incoming_normalized(%{"type" => "Dislike"} = data, options) do + data + |> Map.put("type", "EmojiReact") + |> Map.put("content", "👎") + |> handle_incoming_normalized(options) + end + + defp handle_incoming_normalized( + %{"type" => "Undo", "object" => %{"type" => "Dislike"}} = data, + options + ) do + data + |> put_in(["object", "type"], "EmojiReact") + |> put_in(["object", "content"], "👎") + |> handle_incoming_normalized(options) + end + defp handle_incoming_normalized(_, _), do: :error @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index 0f22dd538..b35f5cdcd 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -335,13 +335,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do if params["password"] do User.force_password_reset_async(user) - end - ModerationLog.insert_log(%{ - actor: admin, - subject: [user], - action: "force_password_reset" - }) + ModerationLog.insert_log(%{ + actor: admin, + subject: [user], + action: "force_password_reset" + }) + end json(conn, %{status: "success"}) else diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index 63409870e..e5339097f 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -97,7 +97,7 @@ defmodule Pleroma.Web.ApiSpec do "Frontend management", "Instance configuration", "Instance documents", - "Instance rule managment", + "Instance rule management", "Invites", "MediaProxy cache", "OAuth application management", diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 21a779dcb..d63e92d16 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -284,18 +284,6 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do :query, %Schema{allOf: [BooleanLike], default: true}, "Mute notifications in addition to statuses? Defaults to `true`." - ), - Operation.parameter( - :duration, - :query, - %Schema{type: :integer}, - "Expire the mute in `duration` seconds. Default 0 for infinity" - ), - Operation.parameter( - :expires_in, - :query, - %Schema{type: :integer, default: 0}, - "Deprecated, use `duration` instead" ) ], responses: %{ @@ -323,16 +311,37 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do tags: ["Account actions"], summary: "Block", operationId: "AccountController.block", + requestBody: request_body("Parameters", block_request()), security: [%{"oAuth" => ["follow", "write:blocks"]}], description: "Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)", - parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} + ], responses: %{ 200 => Operation.response("Relationship", "application/json", AccountRelationship) } } end + defp block_request do + %Schema{ + title: "AccountBlockRequest", + description: "POST body for blocking an account", + type: :object, + properties: %{ + duration: %Schema{ + type: :integer, + nullable: true, + description: "Expire the mute in `duration` seconds. Default 0 for infinity" + } + }, + example: %{ + "duration" => 86_400 + } + } + end + def unblock_operation do %Operation{ tags: ["Account actions"], diff --git a/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex b/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex index c3a3ecc7c..6d06728f4 100644 --- a/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex @@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do def index_operation do %Operation{ - tags: ["Instance rule managment"], + tags: ["Instance rule management"], summary: "Retrieve list of instance rules", operationId: "AdminAPI.RuleController.index", security: [%{"oAuth" => ["admin:read"]}], @@ -33,7 +33,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do def create_operation do %Operation{ - tags: ["Instance rule managment"], + tags: ["Instance rule management"], summary: "Create new rule", operationId: "AdminAPI.RuleController.create", security: [%{"oAuth" => ["admin:write"]}], @@ -49,7 +49,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do def update_operation do %Operation{ - tags: ["Instance rule managment"], + tags: ["Instance rule management"], summary: "Modify existing rule", operationId: "AdminAPI.RuleController.update", security: [%{"oAuth" => ["admin:write"]}], @@ -65,7 +65,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.RuleOperation do def delete_operation do %Operation{ - tags: ["Instance rule managment"], + tags: ["Instance rule management"], summary: "Delete rule", operationId: "AdminAPI.RuleController.delete", parameters: [Operation.parameter(:id, :path, :string, "Rule ID")], diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 7d7a5ecc1..911ffb994 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -52,7 +52,30 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do summary: "Retrieve list of instance rules", operationId: "InstanceController.rules", responses: %{ - 200 => Operation.response("Array of domains", "application/json", array_of_rules()) + 200 => Operation.response("Array of rules", "application/json", array_of_rules()) + } + } + end + + def translation_languages_operation do + %Operation{ + tags: ["Instance misc"], + summary: "Retrieve supported languages matrix", + operationId: "InstanceController.translation_languages", + responses: %{ + 200 => + Operation.response( + "Translation languages matrix", + "application/json", + %Schema{ + type: :object, + additionalProperties: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: "Supported target languages for a source language" + } + } + ) } } end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex index f595583b6..6f77584a8 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex @@ -59,11 +59,15 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do album: %Schema{type: :string, description: "The album of the media playing"}, artist: %Schema{type: :string, description: "The artist of the media playing"}, length: %Schema{type: :integer, description: "The length of the media playing"}, - externalLink: %Schema{type: :string, description: "A URL referencing the media playing"}, + external_link: %Schema{type: :string, description: "A URL referencing the media playing"}, visibility: %Schema{ allOf: [VisibilityScope], default: "public", description: "Scrobble visibility" + }, + externalLink: %Schema{ + type: :string, + description: "Deprecated, use `external_link` instead" } }, example: %{ @@ -71,7 +75,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do "artist" => "Some Artist", "album" => "Some Album", "length" => 180_000, - "externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title" + "external_link" => "https://www.last.fm/music/Some+Artist/_/Some+Title" } } end @@ -85,7 +89,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do title: %Schema{type: :string, description: "The title of the media playing"}, album: %Schema{type: :string, description: "The album of the media playing"}, artist: %Schema{type: :string, description: "The artist of the media playing"}, - externalLink: %Schema{type: :string, description: "A URL referencing the media playing"}, + external_link: %Schema{type: :string, description: "A URL referencing the media playing"}, length: %Schema{ type: :integer, description: "The length of the media playing", @@ -100,7 +104,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do "artist" => "Some Artist", "album" => "Some Album", "length" => 180_000, - "externalLink" => "https://www.last.fm/music/Some+Artist/_/Some+Title", + "external_link" => "https://www.last.fm/music/Some+Artist/_/Some+Title", "created_at" => "2019-09-28T12:40:45.000Z" } } diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index ef828feee..75ecda321 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -427,6 +427,38 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do } end + def translate_operation do + %Operation{ + tags: ["Retrieve status information"], + summary: "Translate status", + description: "Translate status with an external API", + operationId: "StatusController.translate", + security: [%{"oAuth" => ["read:statuses"]}], + parameters: [id_param()], + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + properties: %{ + lang: %Schema{ + type: :string, + nullable: true, + description: "Translation target language." + } + } + }, + required: false + ), + responses: %{ + 200 => Operation.response("Translation", "application/json", translation()), + 400 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError), + 503 => Operation.response("Error", "application/json", ApiError) + } + } + end + def favourites_operation do %Operation{ tags: ["Timelines"], @@ -819,4 +851,32 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do } } end + + defp translation do + %Schema{ + title: "StatusTranslation", + description: "Represents status translation with related information.", + type: :object, + required: [:content, :detected_source_language, :provider], + properties: %{ + content: %Schema{ + type: :string, + description: "Translated status content" + }, + detected_source_language: %Schema{ + type: :string, + description: "Detected source language" + }, + provider: %Schema{ + type: :string, + description: "Translation provider service name" + } + }, + example: %{ + "content" => "Software für die nächste Generation der sozialen Medien.", + "detected_source_language" => "en", + "provider" => "Deepl" + } + } + end end diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 1f73ef60c..19827e996 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -34,6 +34,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do id: FlakeID, locked: %Schema{type: :boolean}, mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true}, + block_expires_at: %Schema{type: :string, format: "date-time", nullable: true}, note: %Schema{type: :string, format: :html}, statuses_count: %Schema{type: :integer}, url: %Schema{type: :string, format: :uri}, diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 412424dae..ae554d0b9 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -27,9 +27,9 @@ defmodule Pleroma.Web.CommonAPI do require Logger @spec block(User.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors() - def block(blocked, blocker) do - with {:ok, block_data, _} <- Builder.block(blocker, blocked), - {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do + def block(blocked, blocker, params \\ %{}) do + with {:ok, block_data, meta} <- Builder.block(blocker, blocked, params), + {:ok, block, _} <- Pipeline.common_pipeline(block_data, meta ++ [local: true]) do {:ok, block} end end diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 4220757df..f60ed8b02 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do alias Pleroma.Activity alias Pleroma.Conversation.Participation + alias Pleroma.Language.LanguageDetector alias Pleroma.Object alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Visibility @@ -90,7 +91,8 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do defp listen_object(draft) do object = draft.params - |> Map.take([:album, :artist, :title, :length, :externalLink]) + |> Map.take([:album, :artist, :title, :length]) + |> Map.put(:externalLink, Map.get(draft.params, :external_link)) |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", "Audio") |> Map.put("to", draft.to) @@ -255,13 +257,15 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do end defp language(draft) do - language = draft.params[:language] + language = + with language <- draft.params[:language], + true <- good_locale_code?(language) do + language + else + _ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary) + end - if good_locale_code?(language) do - %__MODULE__{draft | language: language} - else - draft - end + %__MODULE__{draft | language: language} end defp object(draft) do diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex index 58260afa8..676fc5137 100644 --- a/lib/pleroma/web/federator.ex +++ b/lib/pleroma/web/federator.ex @@ -122,6 +122,10 @@ defmodule Pleroma.Web.Federator do Logger.debug("Unhandled actor #{actor}, #{inspect(e)}") {:error, e} + {:reject, reason} = e -> + Logger.debug("Rejected by MRF: #{inspect(reason)}") + {:error, e} + e -> # Just drop those for now Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end) diff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex index 9da3c5008..143a0b0b8 100644 --- a/lib/pleroma/web/instance_document.ex +++ b/lib/pleroma/web/instance_document.ex @@ -46,7 +46,7 @@ defmodule Pleroma.Web.InstanceDocument do defp put_file(origin_path, destination_path) do with destination <- instance_static_dir(destination_path), - {_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))}, + {_, :ok} <- {:mkdir_p, Pleroma.Backports.mkdir_p(Path.dirname(destination))}, {_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do :ok else diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 68157b0c4..d374e8c01 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -501,8 +501,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end @doc "POST /api/v1/accounts/:id/block" - def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do - with {:ok, _activity} <- CommonAPI.block(blocked, blocker) do + def block( + %{ + assigns: %{user: blocker, account: blocked}, + private: %{open_api_spex: %{body_params: params}} + } = conn, + _params + ) do + with {:ok, _activity} <- CommonAPI.block(blocked, blocker, params) do render(conn, "relationship.json", user: blocker, target: blocked) else {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -607,7 +613,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do users: users, for: user, as: :user, - embed_relationships: embed_relationships?(params) + embed_relationships: embed_relationships?(params), + blocks: true ) end diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex index b97b0e476..0f74c1dff 100644 --- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -30,4 +30,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do def rules(conn, _params) do render(conn, "rules.json") end + + @doc "GET /api/v1/instance/translation_languages" + def translation_languages(conn, _params) do + render(conn, "translation_languages.json") + end end diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index 628aa311b..d9a1ba41e 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -190,7 +190,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do f.() rescue error -> - Logger.error("#{__MODULE__} search error: #{inspect(error)}") + Logger.error(Exception.format(:error, error, __STACKTRACE__)) fallback end end diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index d5aef5ad2..10549fb20 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do alias Pleroma.Activity alias Pleroma.Bookmark alias Pleroma.BookmarkFolder + alias Pleroma.Language.Translation alias Pleroma.Object alias Pleroma.Repo alias Pleroma.ScheduledActivity @@ -44,6 +45,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do ] ) + plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :translate) + plug( OAuthScopesPlug, %{scopes: ["write:statuses"]} @@ -85,7 +88,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark] ) - @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a + @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete translate)a plug( RateLimiter, @@ -549,6 +552,41 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do end end + @doc "POST /api/v1/statuses/:id/translate" + def translate( + %{ + assigns: %{user: user}, + private: %{open_api_spex: %{body_params: params, params: %{id: status_id}}} + } = conn, + _ + ) do + with %Activity{object: object} <- Activity.get_by_id_with_object(status_id), + {:visibility, visibility} when visibility in ["public", "unlisted"] <- + {:visibility, Visibility.get_visibility(object)}, + {:language, language} when is_binary(language) <- + {:language, Map.get(params, :lang) || user.language}, + {:ok, result} <- + Translation.translate( + object.data["content"], + object.data["language"], + language + ) do + render(conn, "translation.json", result) + else + {:language, nil} -> + render_error(conn, :bad_request, "Language not specified") + + {:visibility, _} -> + render_error(conn, :not_found, "Record not found") + + {:error, :not_found} -> + render_error(conn, :not_found, "Translation service not configured") + + {:error, error} when error in [:unexpected_response, :quota_exceeded, :too_many_requests] -> + render_error(conn, :service_unavailable, "Translation service not available") + end + end + @doc "GET /api/v1/favourites" def favourites( %{assigns: %{user: %User{} = user}, private: %{open_api_spex: %{params: params}}} = conn, diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index f6727d29d..8d28dd69a 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -340,6 +340,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do |> maybe_put_unread_notification_count(user, opts[:for]) |> maybe_put_email_address(user, opts[:for]) |> maybe_put_mute_expires_at(user, opts[:for], opts) + |> maybe_put_block_expires_at(user, opts[:for], opts) |> maybe_show_birthday(user, opts[:for]) end @@ -476,6 +477,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp maybe_put_mute_expires_at(data, _, _, _), do: data + defp maybe_put_block_expires_at(data, %User{} = user, target, %{blocks: true}) do + Map.put( + data, + :block_expires_at, + UserRelationship.get_block_expire_date(target, user) + ) + end + + defp maybe_put_block_expires_at(data, _, _, _), do: data + defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do data |> Kernel.put_in([:pleroma, :birthday], user.birthday) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 913684928..1b6f26af7 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -90,6 +90,15 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do } end + def render("translation_languages.json", _) do + with true <- Pleroma.Language.Translation.configured?(), + {:ok, languages} <- Pleroma.Language.Translation.languages_matrix() do + languages + else + _ -> %{} + end + end + defp common_information(instance) do %{ languages: Keyword.get(instance, :languages, ["en"]), @@ -145,7 +154,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do end, "pleroma:get:main/ostatus", "pleroma:group_actors", - "pleroma:bookmark_folders" + "pleroma:bookmark_folders", + if Pleroma.Language.LanguageDetector.configured?() do + "pleroma:language_detection" + end, + "pleroma:block_expiration" ] |> Enum.filter(& &1) end @@ -243,11 +256,27 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do }, vapid: %{ public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - } + }, + translation: %{enabled: Pleroma.Language.Translation.configured?()} }) end defp pleroma_configuration(instance) do + base_urls = %{} + + base_urls = + if Config.get([:media_proxy, :enabled]) do + Map.put(base_urls, :media_proxy, Config.get([:media_proxy, :base_url])) + else + base_urls + end + + base_urls = + case Config.get([Pleroma.Upload, :base_url]) do + nil -> base_urls + url -> Map.put(base_urls, :upload, url) + end + %{ metadata: %{ account_activation_required: Keyword.get(instance, :account_activation_required), @@ -256,7 +285,10 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do fields_limits: fields_limits(), post_formats: Config.get([:instance, :allowed_post_formats]), birthday_required: Config.get([:instance, :birthday_required]), - birthday_min_age: Config.get([:instance, :birthday_min_age]) + birthday_min_age: Config.get([:instance, :birthday_min_age]), + translation: supported_languages(), + base_urls: base_urls, + markup: markup() }, stats: %{mau: Pleroma.User.active_user_count()}, vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) @@ -282,4 +314,37 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do }) }) end + + defp supported_languages do + enabled = Pleroma.Language.Translation.configured?() + + source_languages = + with true <- enabled, + {:ok, languages} <- Pleroma.Language.Translation.supported_languages(:source) do + languages + else + _ -> nil + end + + target_languages = + with true <- enabled, + {:ok, languages} <- Pleroma.Language.Translation.supported_languages(:target) do + languages + else + _ -> nil + end + + %{ + source_languages: source_languages, + target_languages: target_languages + } + end + + defp markup do + %{ + allow_inline_images: Config.get([:markup, :allow_inline_images]), + allow_headings: Config.get([:markup, :allow_headings]), + allow_tables: Config.get([:markup, :allow_tables]) + } + end end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 10966edd6..4b5ac9c3b 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -681,6 +681,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do } end + def render("translation.json", %{ + content: content, + detected_source_language: detected_source_language, + provider: provider + }) do + %{content: content, detected_source_language: detected_source_language, provider: provider} + end + def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do object = Object.normalize(activity, fetch: false) diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index bf6dc500c..5f5f7643f 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -24,6 +24,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation def create(%{assigns: %{user: user}, body_params: params} = conn, _) do + params = + params + |> Map.put_new(:external_link, Map.get(params, :externalLink)) + with {:ok, activity} <- CommonAPI.listen(user, params) do render(conn, "show.json", activity: activity, for: user) else diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex index edf0a2390..51828ad97 100644 --- a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex +++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex @@ -27,8 +27,10 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleView do title: object.data["title"] |> HTML.strip_tags(), artist: object.data["artist"] |> HTML.strip_tags(), album: object.data["album"] |> HTML.strip_tags(), - externalLink: object.data["externalLink"], - length: object.data["length"] + external_link: object.data["externalLink"], + length: object.data["length"], + # DEPRECATED + externalLink: object.data["externalLink"] } end diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index a3a522d7a..9c8ec7a9f 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.RichMedia.Parser do alias Pleroma.Web.RichMedia.Helpers + import Pleroma.Web.Metadata.Utils, only: [scrub_html_and_truncate: 2] require Logger @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) @@ -63,8 +64,20 @@ defmodule Pleroma.Web.RichMedia.Parser do not match?({:ok, _}, Jason.encode(%{key => val})) end) |> Map.new() + |> truncate_title() + |> truncate_desc() end + defp truncate_title(%{"title" => title} = data) when is_binary(title), + do: %{data | "title" => scrub_html_and_truncate(title, 120)} + + defp truncate_title(data), do: data + + defp truncate_desc(%{"description" => desc} = data) when is_binary(desc), + do: %{data | "description" => scrub_html_and_truncate(desc, 200)} + + defp truncate_desc(data), do: data + @spec validate_page_url(URI.t() | binary()) :: :ok | :error defp validate_page_url(page_url) when is_binary(page_url) do validate_tld = @config_impl.get([Pleroma.Formatter, :validate_tld]) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index bf8ebf3e4..dfab1b216 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -189,7 +189,7 @@ defmodule Pleroma.Web.Router do end pipeline :well_known do - plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml"]) + plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml", "html"]) end pipeline :config do @@ -740,6 +740,7 @@ defmodule Pleroma.Web.Router do post("/statuses/:id/unbookmark", StatusController, :unbookmark) post("/statuses/:id/mute", StatusController, :mute_conversation) post("/statuses/:id/unmute", StatusController, :unmute_conversation) + post("/statuses/:id/translate", StatusController, :translate) post("/push/subscription", SubscriptionController, :create) get("/push/subscription", SubscriptionController, :show) @@ -787,6 +788,7 @@ defmodule Pleroma.Web.Router do get("/instance", InstanceController, :show) get("/instance/peers", InstanceController, :peers) get("/instance/rules", InstanceController, :rules) + get("/instance/translation_languages", InstanceController, :translation_languages) get("/statuses", StatusController, :index) get("/statuses/:id", StatusController, :show) diff --git a/lib/pleroma/web/templates/email/digest.html.eex b/lib/pleroma/web/templates/email/digest.html.eex index 1efc76e1a..d2918bc6f 100644 --- a/lib/pleroma/web/templates/email/digest.html.eex +++ b/lib/pleroma/web/templates/email/digest.html.eex @@ -231,8 +231,8 @@ <%= for %{data: mention, object: object, from: from} <- @mentions do %> - <%# mention START %> - <%# user card START %> + <% # mention START %> + <% # user card START %>
@@ -291,7 +291,7 @@
- <%# user card END %> + <% # user card END %>
- <%# mention END %> + <% # mention END %> <% end %> <%= if @followers != [] do %> - <%# new followers header START %> + <% # new followers header START %>
@@ -397,10 +397,10 @@
- <%# new followers header END %> + <% # new followers header END %> <%= for %{data: follow, from: from} <- @followers do %> - <%# user card START %> + <% # user card START %>
@@ -459,13 +459,13 @@
- <%# user card END %> + <% # user card END %> <% end %> <% end %> - <%# divider start %> + <% # divider start %>
@@ -514,7 +514,7 @@
- <%# divider end %> + <% # divider end %>
diff --git a/lib/pleroma/web/templates/email/new_users_digest.html.eex b/lib/pleroma/web/templates/email/new_users_digest.html.eex index 40d9b8381..78b8ac4f9 100644 --- a/lib/pleroma/web/templates/email/new_users_digest.html.eex +++ b/lib/pleroma/web/templates/email/new_users_digest.html.eex @@ -1,5 +1,5 @@ <%= for {user, total_statuses, latest_status} <- @users_and_statuses do %> - <%# user card START %> + <% # user card START %>
@@ -60,7 +60,7 @@
- <%# user card END %> + <% # user card END %> <%= if latest_status do %>
@@ -104,7 +104,7 @@
<% end %> - <%# divider start %> + <% # divider start %>
@@ -153,6 +153,6 @@
- <%# divider end %> - <%# user card END %> + <% # divider end %> + <% # user card END %> <% end %> diff --git a/lib/pleroma/web/templates/layout/email_styled.html.eex b/lib/pleroma/web/templates/layout/email_styled.html.eex index 82cabd889..a1ed4ece3 100644 --- a/lib/pleroma/web/templates/layout/email_styled.html.eex +++ b/lib/pleroma/web/templates/layout/email_styled.html.eex @@ -111,7 +111,7 @@ - <%# header %> + <% # header %>
@@ -145,7 +145,7 @@
- <%# title %> + <% # title %> <%= if @title do %>
<%= for scope <- @available_scopes do %> - <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> + <% # Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> <%= if scope in @scopes do %>
<%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex index 021df9bc5..8a291e28e 100644 --- a/lib/pleroma/web/web_finger/web_finger_controller.ex +++ b/lib/pleroma/web/web_finger/web_finger_controller.ex @@ -41,5 +41,10 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do end end + # Default to JSON when no format is specified or format is not recognized + def webfinger(%{assigns: %{format: _format}} = conn, %{"resource" => _resource} = params) do + webfinger(put_in(conn.assigns.format, "json"), params) + end + def webfinger(conn, _params), do: send_resp(conn, 400, "Bad Request") end diff --git a/lib/pleroma/workers/mute_expire_worker.ex b/lib/pleroma/workers/mute_expire_worker.ex index 8356a775d..9a04fc486 100644 --- a/lib/pleroma/workers/mute_expire_worker.ex +++ b/lib/pleroma/workers/mute_expire_worker.ex @@ -5,9 +5,13 @@ defmodule Pleroma.Workers.MuteExpireWorker do use Oban.Worker, queue: :background + alias Pleroma.User + @impl true - def perform(%Job{args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}}) do - Pleroma.User.unmute(muter_id, mutee_id) + def perform(%Job{ + args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id} + }) do + User.unmute(muter_id, mutee_id) :ok end @@ -18,6 +22,17 @@ defmodule Pleroma.Workers.MuteExpireWorker do :ok end + def perform(%Job{ + args: %{"op" => "unblock_user", "blocker_id" => blocker_id, "blocked_id" => blocked_id} + }) do + Pleroma.Web.CommonAPI.unblock( + User.get_cached_by_id(blocked_id), + User.get_cached_by_id(blocker_id) + ) + + :ok + end + @impl true def timeout(_job), do: :timer.seconds(5) end diff --git a/mix.exs b/mix.exs index 808a2b12c..971084f94 100644 --- a/mix.exs +++ b/mix.exs @@ -37,22 +37,13 @@ defmodule Pleroma.Mixfile do pleroma: [ include_executables_for: [:unix], applications: [ex_syslogger: :load, syslog: :load, eldap: :transient], - steps: [:assemble, &put_otp_version/1, ©_files/1, ©_nginx_config/1], + steps: [:assemble, ©_files/1, ©_nginx_config/1], config_providers: [{Pleroma.Config.ReleaseRuntimeProvider, []}] ] ] ] end - def put_otp_version(%{path: target_path} = release) do - File.write!( - Path.join([target_path, "OTP_VERSION"]), - Pleroma.OTPVersion.version() - ) - - release - end - def copy_files(%{path: target_path} = release) do File.cp_r!("./rel/files", target_path) release @@ -213,7 +204,7 @@ defmodule Pleroma.Mixfile do {:poison, "~> 3.0", only: :test}, {:ex_doc, "~> 0.22", only: :dev, runtime: false}, {:ex_machina, "~> 2.4", only: :test}, - {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.5", only: :test}, {:covertool, "~> 2.0", only: :test}, {:hackney, "~> 1.18.0", override: true}, @@ -236,7 +227,7 @@ defmodule Pleroma.Mixfile do "ecto.rollback": ["pleroma.ecto.rollback"], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], - test: ["ecto.create --quiet", "ecto.migrate", "test"], + test: ["ecto.create --quiet", "ecto.migrate", "test --warnings-as-errors"], docs: ["pleroma.docs", "docs"], analyze: ["credo --strict --only=warnings,todo,fixme,consistency,readability"], copyright: &add_copyright/1, diff --git a/mix.lock b/mix.lock index 9b53ede62..f7f37b7e1 100644 --- a/mix.lock +++ b/mix.lock @@ -23,7 +23,7 @@ "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, - "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, diff --git a/priv/repo/migrations/20240904142434_assign_app_user.exs b/priv/repo/migrations/20240904142434_assign_app_user.exs index 11bec529b..74740220d 100644 --- a/priv/repo/migrations/20240904142434_assign_app_user.exs +++ b/priv/repo/migrations/20240904142434_assign_app_user.exs @@ -1,20 +1,24 @@ defmodule Pleroma.Repo.Migrations.AssignAppUser do use Ecto.Migration + import Ecto.Query + alias Pleroma.Repo alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Token def up do - Repo.all(Token) - |> Enum.group_by(fn x -> Map.get(x, :app_id) end) - |> Enum.each(fn {_app_id, tokens} -> - token = - Enum.filter(tokens, fn x -> not is_nil(x.user_id) end) - |> List.first() - + Token + |> where([t], not is_nil(t.user_id)) + |> group_by([t], t.app_id) + |> select([t], %{app_id: t.app_id, id: min(t.id)}) + |> order_by(asc: :app_id) + |> Repo.stream() + |> Stream.each(fn %{id: id} -> + token = Token.Query.get_by_id(id) |> Repo.one() App.maybe_update_owner(token) end) + |> Stream.run() end def down, do: :ok diff --git a/priv/repo/migrations/20250314153704_add_activities_actor_type_index.exs b/priv/repo/migrations/20250314153704_add_activities_actor_type_index.exs new file mode 100644 index 000000000..a0fac28a8 --- /dev/null +++ b/priv/repo/migrations/20250314153704_add_activities_actor_type_index.exs @@ -0,0 +1,14 @@ +defmodule Pleroma.Repo.Migrations.AddActivitiesActorTypeIndex do + use Ecto.Migration + @disable_ddl_transaction true + + def change do + create( + index( + :activities, + ["actor", "(data ->> 'type'::text)", "id DESC NULLS LAST"], + concurrently: true + ) + ) + end +end diff --git a/test/fixtures/friendica-dislike-undo.json b/test/fixtures/friendica-dislike-undo.json new file mode 100644 index 000000000..b258e00be --- /dev/null +++ b/test/fixtures/friendica-dislike-undo.json @@ -0,0 +1,76 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Hashtag": "as:Hashtag", + "PropertyValue": "schema:PropertyValue", + "conversation": "ostatus:conversation", + "dfrn": "http://purl.org/macgirvin/dfrn/1.0/", + "diaspora": "https://diasporafoundation.org/ns/", + "directMessage": "litepub:directMessage", + "discoverable": "toot:discoverable", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "litepub": "http://litepub.social/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "ostatus": "http://ostatus.org#", + "quoteUrl": "as:quoteUrl", + "schema": "http://schema.org#", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "value": "schema:value", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "actor": "https://my-place.social/profile/vaartis", + "cc": [ + "https://my-place.social/followers/vaartis" + ], + "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182/Undo", + "instrument": { + "id": "https://my-place.social/friendica", + "name": "Friendica 'Interrupted Fern' 2024.12-1576", + "type": "Application", + "url": "https://my-place.social" + }, + "object": { + "actor": "https://my-place.social/profile/vaartis", + "cc": [ + "https://my-place.social/followers/vaartis" + ], + "diaspora:guid": "e599373b-1968-4b20-cd24-80d340160302", + "diaspora:like": "{\"author\":\"vaartis@my-place.social\",\"guid\":\"e599373b-1968-4b20-cd24-80d340160302\",\"parent_guid\":\"cd36feba-c31f3ed3fd5c064a-17c31593\",\"parent_type\":\"Post\",\"positive\":\"false\",\"author_signature\":\"xR2zLJNfc9Nhx1n8LLMWM1kde12my4cqamIsrH\\/UntKzuDwO4DuHBL0fkFhgC\\/dylxm4HqsHD45MQbtwQCVGq6WhC96TrbMuYEK61HIO23dTr3m+qJVtfdH4wyhUNHgiiYPhZpkLDfnR1JiRWmFTlmZC8q8JEkOB5IQsrWia2eOR6IsqDcdKO\\/Urgns9\\/BdQi8KnchBKSEFc1iUtcOEruvhunKGyW5zI\\/Rltfdz3xGH8tlw+YlMXeWXPnqgOJ9GzNA0lwG4U421L6yylYagW7oxIznnBLB4bO46vYZbgXZV1hiI9ZyveHOinLMY1QkmTj5CNvnx3\\/VJwLovd0v+0Nr2vu\\/3ftbpBXc6L1bsNjlRqtsfwJlcgl+tH1DC4W8tKf+Y3tdtzVw0CHXCuacxHLyq5wZd\\/5YfYR9SJQ\\/jInU4PHA5+hIE3PGqNUp5QfFE0umq56H7MQKsIPgM5mMV4fPAA8OpltuMVDvQYUxalrnvoTf00k90x1wCTK71\\/jQGh7r7PmGvSdfPr+65eVTjITD8\\/lTGIb8850v1fl3\\/i2R8Dv17jQIRyX5o9MXPSO6jHo4Swma5RzPA\\/0bRj6qRTyPkM1L9qEIr+2H2I7KKhT2ZE5GhAU7yI9A3VLBWzpTrUPMGbfpd1OjVTEqXAdMjpLDYI3Mh5zQ58p8xCzt+W+t0=\"}", + "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182", + "instrument": { + "id": "https://my-place.social/friendica", + "name": "Friendica 'Interrupted Fern' 2024.12-1576", + "type": "Application", + "url": "https://my-place.social" + }, + "object": "https://pl.kotobank.ch/objects/301bce65-8a1b-4c49-a65c-fe2ce861a213", + "published": "2025-06-12T18:47:41Z", + "to": [ + "https://pl.kotobank.ch/users/vaartis", + "https://mitra.social/users/silverpill", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Dislike" + }, + "published": "2025-06-12T18:41:25Z", + "signature": { + "created": "2025-06-12T18:44:16Z", + "creator": "https://my-place.social/profile/vaartis#main-key", + "nonce": "2d67847d4bd4b1b83a30d61eac6cdc7ad6b980df06a8b9b97217e1d8f7b6cf20", + "signatureValue": "LnoRMZuQGDvTICkShGBq28ynaj2lF1bViJFGS6n4gKn3IbxPWATHxao43gxWRc+HCTrHNg7quzgaW4+PYM7UVUz3jO+bjNKsN845nijOVdyFrPOXbuaij3KQh2OoHhFJWoV/ZQQTFF0kRK1qT4BwG+P8NqOOKAMv+Cw7ruQH+f2w7uDgcNIbCD1gLcwb6cw7WVe5qu8yMkKqp2kBdqW3RCsI85RmmFgwehDgH5nrX7ER1qbeLWrqy7echwD9/fO3rqAu13xDNyiGZHDT7JB3RUt0AyMm0XCfjbwSQ0n+MkYXgE4asvFz81+iiPCLt+6gePWAFc5odF1FxdySBpSuUOs4p92NzP9OhQ0c0qrqrzYI7aYklY7oMfxjkva+X+0bm3up+2IRJdnZa/pXlmwdcqTpyMr1sgzaexMUNBp3dq7zA51eEaakLDX3i2onXJowfmze3+6XgPAFHYamR+pRNtuEoY4uyYEK3fj5GgwJ4RtFJMYVoEs/Q8h3OgYRcK1FE9UlDjSqbQ7QIRn2Ib4wjgmkeM0vrHIwh/1CtqA/M/6WuYFzCaJBc8O9ykpK9ZMbw64ToQXKf2SqhZsDoyTWRWTO1PXOk1XCAAElUh8/WCyeghvgqLXn0LHov4lmBsHA5iMUcLqBKD3GJIHd+ExrOFxMZs4mBLLGyz0p5joJ3NY=", + "type": "RsaSignature2017" + }, + "to": [ + "https://pl.kotobank.ch/users/vaartis", + "https://mitra.social/users/silverpill", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Undo" +} diff --git a/test/fixtures/friendica-dislike.json b/test/fixtures/friendica-dislike.json new file mode 100644 index 000000000..c75939073 --- /dev/null +++ b/test/fixtures/friendica-dislike.json @@ -0,0 +1,56 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Hashtag": "as:Hashtag", + "PropertyValue": "schema:PropertyValue", + "conversation": "ostatus:conversation", + "dfrn": "http://purl.org/macgirvin/dfrn/1.0/", + "diaspora": "https://diasporafoundation.org/ns/", + "directMessage": "litepub:directMessage", + "discoverable": "toot:discoverable", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "litepub": "http://litepub.social/ns#", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "ostatus": "http://ostatus.org#", + "quoteUrl": "as:quoteUrl", + "schema": "http://schema.org#", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "value": "schema:value", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "actor": "https://my-place.social/profile/vaartis", + "cc": [ + "https://my-place.social/followers/vaartis" + ], + "diaspora:guid": "e599373b-1968-4b20-cd24-80d340160302", + "diaspora:like": "{\"author\":\"vaartis@my-place.social\",\"guid\":\"e599373b-1968-4b20-cd24-80d340160302\",\"parent_guid\":\"cd36feba-c31f3ed3fd5c064a-17c31593\",\"parent_type\":\"Post\",\"positive\":\"false\",\"author_signature\":\"xR2zLJNfc9Nhx1n8LLMWM1kde12my4cqamIsrH\\/UntKzuDwO4DuHBL0fkFhgC\\/dylxm4HqsHD45MQbtwQCVGq6WhC96TrbMuYEK61HIO23dTr3m+qJVtfdH4wyhUNHgiiYPhZpkLDfnR1JiRWmFTlmZC8q8JEkOB5IQsrWia2eOR6IsqDcdKO\\/Urgns9\\/BdQi8KnchBKSEFc1iUtcOEruvhunKGyW5zI\\/Rltfdz3xGH8tlw+YlMXeWXPnqgOJ9GzNA0lwG4U421L6yylYagW7oxIznnBLB4bO46vYZbgXZV1hiI9ZyveHOinLMY1QkmTj5CNvnx3\\/VJwLovd0v+0Nr2vu\\/3ftbpBXc6L1bsNjlRqtsfwJlcgl+tH1DC4W8tKf+Y3tdtzVw0CHXCuacxHLyq5wZd\\/5YfYR9SJQ\\/jInU4PHA5+hIE3PGqNUp5QfFE0umq56H7MQKsIPgM5mMV4fPAA8OpltuMVDvQYUxalrnvoTf00k90x1wCTK71\\/jQGh7r7PmGvSdfPr+65eVTjITD8\\/lTGIb8850v1fl3\\/i2R8Dv17jQIRyX5o9MXPSO6jHo4Swma5RzPA\\/0bRj6qRTyPkM1L9qEIr+2H2I7KKhT2ZE5GhAU7yI9A3VLBWzpTrUPMGbfpd1OjVTEqXAdMjpLDYI3Mh5zQ58p8xCzt+W+t0=\"}", + "id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182", + "instrument": { + "id": "https://my-place.social/friendica", + "name": "Friendica 'Interrupted Fern' 2024.12-1576", + "type": "Application", + "url": "https://my-place.social" + }, + "object": "https://pl.kotobank.ch/objects/301bce65-8a1b-4c49-a65c-fe2ce861a213", + "published": "2025-06-12T18:47:41Z", + "signature": { + "created": "2025-06-12T18:47:42Z", + "creator": "https://my-place.social/profile/vaartis#main-key", + "nonce": "84e496f80b09d7a299c5cc89e8cadd13abf621b3a0a321684fa74278b68a6dd8", + "signatureValue": "qe2WxY+j7daIYLRadCctgal6A1s9XgoiMfM/8KjJm15w0sSizYYqruyQ5gS44e+cj5GHc9v5gP2ieod5v7eHAPzlcDI4bfkcyHVapAXTqU67ZebW+v6Q+21IMDgqrkYCv5TbV7LTxltW59dlqovpHE4TEe/M7xLKWJ3vVchRUcWqH9kDmak0nacoqYVAb5E9jYnQhUWPTCfPV82qQpeWQPOZ4iIvPw6rDkSSY5jL6bCogBZblHGpUjXfe/FPlacaCWiTQdoga3yOBXB1RYPw9nh5FI5Xkv/oi+52WmJrECinlD6AL8/BpiYvKz236zy7p/TR4BXlCx9RR/msjOnSabkQ4kmYFrRr80UDCGF+CdkdzLl8K9rSE3PbF1+nEqD7X0GOWn/DdtixqXJw6IR4bh32YW2SlcrSRBvI1p82Mv68BeqRaYqL6FAhKFwLhX5JpXngZ3k0g7rWWxc498voPWnFZDyCTRNxO9VIIUavDDEQ0BdFk6WDb8zx9tsAg8JoK57eVDcFly7tfVQffYiHpve06d8ag1DtzipqguRsURmuqpGNMq28XBTxwtrP2LnXXHKxoYN/YQ9cDnCKclbx7/uKmOVMLkLZlM0wAVoZpm5z2fG4voKqFiGZ1PoiFY2sN4URMArJtygV3PlTX4ASAQrak0ksvEo9msrBUD0Su9c=", + "type": "RsaSignature2017" + }, + "to": [ + "https://pl.kotobank.ch/users/vaartis", + "https://mitra.social/users/silverpill", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Dislike" +} diff --git a/test/fixtures/misskey-custom-emoji-like.json b/test/fixtures/misskey-custom-emoji-like.json new file mode 100644 index 000000000..51a825d42 --- /dev/null +++ b/test/fixtures/misskey-custom-emoji-like.json @@ -0,0 +1,54 @@ + { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Emoji": "toot:Emoji", + "Hashtag": "as:Hashtag", + "PropertyValue": "schema:PropertyValue", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_summary": "misskey:_misskey_summary", + "_misskey_votes": "misskey:_misskey_votes", + "backgroundUrl": "sharkey:backgroundUrl", + "discoverable": "toot:discoverable", + "featured": "toot:featured", + "fedibird": "http://fedibird.com/ns#", + "firefish": "https://joinfirefish.org/ns#", + "isCat": "misskey:isCat", + "listenbrainz": "sharkey:listenbrainz", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "misskey": "https://misskey-hub.net/ns#", + "quoteUri": "fedibird:quoteUri", + "quoteUrl": "as:quoteUrl", + "schema": "http://schema.org#", + "sensitive": "as:sensitive", + "sharkey": "https://joinsharkey.org/ns#", + "speakAsCat": "firefish:speakAsCat", + "toot": "http://joinmastodon.org/ns#", + "value": "schema:value", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "_misskey_reaction": ":blobwtfnotlikethis:", + "actor": "https://mai.waifuism.life/users/9otxaeemjqy70001", + "content": ":blobwtfnotlikethis:", + "id": "https://mai.waifuism.life/likes/9q2xifhrdnb0001b", + "object": "https://bungle.online/notes/9q2xi2sy4k", + "tag": [ + { + "icon": { + "mediaType": "image/png", + "type": "Image", + "url": "https://mai.waifuism.life/files/1b0510f2-1fb4-43f5-a399-10053bbd8f0f" + }, + "id": "https://mai.waifuism.life/emojis/blobwtfnotlikethis", + "name": ":blobwtfnotlikethis:", + "type": "Emoji", + "updated": "2024-02-07T02:21:46.497Z" + } + ], + "type": "Like" +} + diff --git a/test/fixtures/mitra-custom-emoji-like.json b/test/fixtures/mitra-custom-emoji-like.json new file mode 100644 index 000000000..4d727febd --- /dev/null +++ b/test/fixtures/mitra-custom-emoji-like.json @@ -0,0 +1,46 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + "https://w3id.org/security/data-integrity/v1", + { + "Emoji": "toot:Emoji", + "Hashtag": "as:Hashtag", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#" + } + ], + "actor": "https://mitra.social/users/silverpill", + "cc": [], + "content": ":ablobcatheartsqueeze:", + "id": "https://mitra.social/activities/like/0195a89a-a3a0-ead4-3a1c-aa6311397cfd", + "object": "https://framapiaf.org/users/peertube/statuses/114182703352270287", + "proof": { + "created": "2025-03-18T09:34:21.610678375Z", + "cryptosuite": "eddsa-jcs-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z5AvpwkXQGFpTneRVDNeF48Jo9qYG6PgrE5HaPPpQNdNyc31ULMN4Vxd4aFXELo4Rk5Y9hd9nDy254xP8v5uGGWp1", + "type": "DataIntegrityProof", + "verificationMethod": "https://mitra.social/users/silverpill#ed25519-key" + }, + "tag": [ + { + "attributedTo": "https://mitra.social/actor", + "icon": { + "mediaType": "image/png", + "type": "Image", + "url": "https://mitra.social/media/a08e153441b25e512ab1b2e8922f5d8cd928322c8b79958cd48297ac722a4117.png" + }, + "id": "https://mitra.social/objects/emojis/ablobcatheartsqueeze", + "name": ":ablobcatheartsqueeze:", + "type": "Emoji", + "updated": "1970-01-01T00:00:00Z" + } + ], + "to": [ + "https://framapiaf.org/users/peertube", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Like" +} + diff --git a/test/fixtures/rich_media/instagram_longtext.html b/test/fixtures/rich_media/instagram_longtext.html new file mode 100644 index 000000000..e833f408c --- /dev/null +++ b/test/fixtures/rich_media/instagram_longtext.html @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + +CAPTURE THE ATLAS | ✨ A Once-in-a-Lifetime Shot: Total Lunar Eclipse + Aurora Substorm! 🔴💚 + +Last Thursday night, under the freezing skies of Northern Alaska, I... | Instagram + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/tesla_mock/deepl-languages-list.json b/test/fixtures/tesla_mock/deepl-languages-list.json new file mode 100644 index 000000000..03d47d2ec --- /dev/null +++ b/test/fixtures/tesla_mock/deepl-languages-list.json @@ -0,0 +1 @@ +[{"language":"BG","name":"Bulgarian","supports_formality":false},{"language":"CS","name":"Czech","supports_formality":false},{"language":"DA","name":"Danish","supports_formality":false},{"language":"DE","name":"German","supports_formality":true},{"language":"EL","name":"Greek","supports_formality":false},{"language":"EN-GB","name":"English (British)","supports_formality":false},{"language":"EN-US","name":"English (American)","supports_formality":false},{"language":"ES","name":"Spanish","supports_formality":true},{"language":"ET","name":"Estonian","supports_formality":false},{"language":"FI","name":"Finnish","supports_formality":false},{"language":"FR","name":"French","supports_formality":true},{"language":"HU","name":"Hungarian","supports_formality":false},{"language":"ID","name":"Indonesian","supports_formality":false},{"language":"IT","name":"Italian","supports_formality":true},{"language":"JA","name":"Japanese","supports_formality":false},{"language":"LT","name":"Lithuanian","supports_formality":false},{"language":"LV","name":"Latvian","supports_formality":false},{"language":"NL","name":"Dutch","supports_formality":true},{"language":"PL","name":"Polish","supports_formality":true},{"language":"PT-BR","name":"Portuguese (Brazilian)","supports_formality":true},{"language":"PT-PT","name":"Portuguese (European)","supports_formality":true},{"language":"RO","name":"Romanian","supports_formality":false},{"language":"RU","name":"Russian","supports_formality":true},{"language":"SK","name":"Slovak","supports_formality":false},{"language":"SL","name":"Slovenian","supports_formality":false},{"language":"SV","name":"Swedish","supports_formality":false},{"language":"TR","name":"Turkish","supports_formality":false},{"language":"UK","name":"Ukrainian","supports_formality":false},{"language":"ZH","name":"Chinese (simplified)","supports_formality":false}] \ No newline at end of file diff --git a/test/fixtures/tesla_mock/deepl-translation.json b/test/fixtures/tesla_mock/deepl-translation.json new file mode 100644 index 000000000..fef7bb215 --- /dev/null +++ b/test/fixtures/tesla_mock/deepl-translation.json @@ -0,0 +1 @@ +{"translations":[{"detected_source_language":"PL","text":"REMOVE THE FOLLOWER!Paste this on your follower. If we get 70% of nk users...they will remove the follower!!!"}]} \ No newline at end of file diff --git a/test/fixtures/warnings/otp_version/21.1 b/test/fixtures/warnings/otp_version/21.1 deleted file mode 100644 index 90cd64c4f..000000000 --- a/test/fixtures/warnings/otp_version/21.1 +++ /dev/null @@ -1 +0,0 @@ -21.1 \ No newline at end of file diff --git a/test/fixtures/warnings/otp_version/22.1 b/test/fixtures/warnings/otp_version/22.1 deleted file mode 100644 index d9b314368..000000000 --- a/test/fixtures/warnings/otp_version/22.1 +++ /dev/null @@ -1 +0,0 @@ -22.1 \ No newline at end of file diff --git a/test/fixtures/warnings/otp_version/22.4 b/test/fixtures/warnings/otp_version/22.4 deleted file mode 100644 index 1da8ccd28..000000000 --- a/test/fixtures/warnings/otp_version/22.4 +++ /dev/null @@ -1 +0,0 @@ -22.4 \ No newline at end of file diff --git a/test/fixtures/warnings/otp_version/23.0 b/test/fixtures/warnings/otp_version/23.0 deleted file mode 100644 index 4266d8634..000000000 --- a/test/fixtures/warnings/otp_version/23.0 +++ /dev/null @@ -1 +0,0 @@ -23.0 \ No newline at end of file diff --git a/test/mix/tasks/pleroma/frontend_test.exs b/test/mix/tasks/pleroma/frontend_test.exs index 6d09f8e36..59ebcec92 100644 --- a/test/mix/tasks/pleroma/frontend_test.exs +++ b/test/mix/tasks/pleroma/frontend_test.exs @@ -11,7 +11,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do @dir "test/frontend_static_test" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) clear_config([:instance, :static_dir], @dir) on_exit(fn -> @@ -50,7 +50,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) previously_existing = Path.join([folder, "temp"]) - File.mkdir_p!(folder) + Pleroma.Backports.mkdir_p!(folder) File.write!(previously_existing, "yey") assert File.exists?(previously_existing) diff --git a/test/mix/tasks/pleroma/instance_test.exs b/test/mix/tasks/pleroma/instance_test.exs index b1c10e03c..5ecb6e445 100644 --- a/test/mix/tasks/pleroma/instance_test.exs +++ b/test/mix/tasks/pleroma/instance_test.exs @@ -7,7 +7,7 @@ defmodule Mix.Tasks.Pleroma.InstanceTest do use Pleroma.DataCase setup do - File.mkdir_p!(tmp_path()) + Pleroma.Backports.mkdir_p!(tmp_path()) on_exit(fn -> File.rm_rf(tmp_path()) diff --git a/test/mix/tasks/pleroma/uploads_test.exs b/test/mix/tasks/pleroma/uploads_test.exs index f3d5aa64f..0aa24807e 100644 --- a/test/mix/tasks/pleroma/uploads_test.exs +++ b/test/mix/tasks/pleroma/uploads_test.exs @@ -62,7 +62,7 @@ defmodule Mix.Tasks.Pleroma.UploadsTest do upload_dir = Config.get([Pleroma.Uploaders.Local, :uploads]) if not File.exists?(upload_dir) || File.ls!(upload_dir) == [] do - File.mkdir_p(upload_dir) + Pleroma.Backports.mkdir_p(upload_dir) Path.join([upload_dir, "file.txt"]) |> File.touch() diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs index 6ab3e657e..b458401a7 100644 --- a/test/pleroma/emoji/pack_test.exs +++ b/test/pleroma/emoji/pack_test.exs @@ -58,7 +58,7 @@ defmodule Pleroma.Emoji.PackTest do test "skips existing emojis when adding from zip file", %{pack: pack} do # First, let's create a test pack with a "bear" emoji test_pack_path = Path.join(@emoji_path, "test_bear_pack") - File.mkdir_p(test_pack_path) + Pleroma.Backports.mkdir_p(test_pack_path) # Create a pack.json file File.write!(Path.join(test_pack_path, "pack.json"), """ diff --git a/test/pleroma/frontend_test.exs b/test/pleroma/frontend_test.exs index c89c56c8c..22e0ffb9a 100644 --- a/test/pleroma/frontend_test.exs +++ b/test/pleroma/frontend_test.exs @@ -9,7 +9,7 @@ defmodule Pleroma.FrontendTest do @dir "test/frontend_static_test" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) clear_config([:instance, :static_dir], @dir) on_exit(fn -> @@ -46,7 +46,7 @@ defmodule Pleroma.FrontendTest do folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) previously_existing = Path.join([folder, "temp"]) - File.mkdir_p!(folder) + Pleroma.Backports.mkdir_p!(folder) File.write!(previously_existing, "yey") assert File.exists?(previously_existing) diff --git a/test/pleroma/language/language_detector_test.exs b/test/pleroma/language/language_detector_test.exs new file mode 100644 index 000000000..ccb81d5bd --- /dev/null +++ b/test/pleroma/language/language_detector_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.LanguageDetectorTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Language.LanguageDetector + alias Pleroma.Language.LanguageDetectorMock + alias Pleroma.StaticStubbedConfigMock + + import Mox + + setup do + # Stub the StaticStubbedConfigMock to return our mock for the provider + StaticStubbedConfigMock + |> stub(:get, fn + [Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock + _other -> nil + end) + + # Stub the LanguageDetectorMock with default implementations + LanguageDetectorMock + |> stub(:missing_dependencies, fn -> [] end) + |> stub(:configured?, fn -> true end) + + :ok + end + + test "it detects text language" do + LanguageDetectorMock + |> expect(:detect, fn _text -> "fr" end) + + detected_language = LanguageDetector.detect("Je viens d'atterrir en Tchéquie.") + + assert detected_language == "fr" + end + + test "it returns nil if text is not long enough" do + # No need to set expectations as the word count check happens before the provider is called + + detected_language = LanguageDetector.detect("it returns nil") + + assert detected_language == nil + end + + test "it returns nil if no provider specified" do + # Override the stub to return nil for the provider + StaticStubbedConfigMock + |> expect(:get, fn [Pleroma.Language.LanguageDetector, :provider] -> nil end) + + detected_language = LanguageDetector.detect("this should also return nil") + + assert detected_language == nil + end +end diff --git a/test/pleroma/language/translation/deepl_test.exs b/test/pleroma/language/translation/deepl_test.exs new file mode 100644 index 000000000..3a7265622 --- /dev/null +++ b/test/pleroma/language/translation/deepl_test.exs @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Language.Translation.DeeplTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Language.Translation.Deepl + + test "it translates text" do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com") + clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY") + + {:ok, res} = + Deepl.translate( + "USUNĄĆ ŚLEDZIKA!Wklej to na swojego śledzika. Jeżeli uzbieramy 70% użytkowników nk...to usuną śledzika!!!", + "pl", + "en" + ) + + assert %{ + detected_source_language: "PL", + provider: "DeepL" + } = res + end + + test "it returns languages list" do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com") + clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY") + + assert {:ok, [language | _languages]} = Deepl.supported_languages(:target) + + assert is_binary(language) + end +end diff --git a/test/pleroma/language/translation_test.exs b/test/pleroma/language/translation_test.exs new file mode 100644 index 000000000..0be7a8d60 --- /dev/null +++ b/test/pleroma/language/translation_test.exs @@ -0,0 +1,28 @@ +defmodule Pleroma.Language.TranslationTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Language.Translation + + setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock) + + test "it translates text" do + assert {:ok, + %{ + content: "txet emos", + detected_source_language: _, + provider: _ + }} = Translation.translate("some text", "en", "uk") + end + + test "it stores translation result in cache" do + Translation.translate("some text", "en", "uk") + + assert {:ok, result} = + Cachex.get( + :translations_cache, + "en/uk/#{:crypto.hash(:sha256, "some text") |> Base.encode64()}" + ) + + assert result.content == "txet emos" + end +end diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs index ed5c2b6c8..13e941e4d 100644 --- a/test/pleroma/object_test.exs +++ b/test/pleroma/object_test.exs @@ -156,7 +156,7 @@ defmodule Pleroma.ObjectTest do uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads]) - File.mkdir_p!(uploads_dir) + Pleroma.Backports.mkdir_p!(uploads_dir) file = %Plug.Upload{ content_type: "image/jpeg", diff --git a/test/pleroma/otp_version_test.exs b/test/pleroma/otp_version_test.exs deleted file mode 100644 index 21701d5a8..000000000 --- a/test/pleroma/otp_version_test.exs +++ /dev/null @@ -1,42 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.OTPVersionTest do - use ExUnit.Case, async: true - - alias Pleroma.OTPVersion - - describe "check/1" do - test "22.4" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.4"]) == - "22.4" - end - - test "22.1" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.1"]) == - "22.1" - end - - test "21.1" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/21.1"]) == - "21.1" - end - - test "23.0" do - assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/23.0"]) == - "23.0" - end - - test "with nonexistent file" do - assert OTPVersion.get_version_from_files([ - "test/fixtures/warnings/otp_version/non-exising", - "test/fixtures/warnings/otp_version/22.4" - ]) == "22.4" - end - - test "empty paths" do - assert OTPVersion.get_version_from_files([]) == nil - end - end -end diff --git a/test/pleroma/safe_zip_test.exs b/test/pleroma/safe_zip_test.exs index 3312d4e63..f07b25675 100644 --- a/test/pleroma/safe_zip_test.exs +++ b/test/pleroma/safe_zip_test.exs @@ -9,12 +9,12 @@ defmodule Pleroma.SafeZipTest do setup do # Ensure tmp directory exists - File.mkdir_p!(@tmp_dir) + Pleroma.Backports.mkdir_p!(@tmp_dir) on_exit(fn -> # Clean up any files created during tests File.rm_rf!(@tmp_dir) - File.mkdir_p!(@tmp_dir) + Pleroma.Backports.mkdir_p!(@tmp_dir) end) :ok @@ -89,7 +89,7 @@ defmodule Pleroma.SafeZipTest do # For this test, we'll manually check if the file exists in the archive # by extracting it and verifying it exists extract_dir = Path.join(@tmp_dir, "extract_check") - File.mkdir_p!(extract_dir) + Pleroma.Backports.mkdir_p!(extract_dir) {:ok, files} = SafeZip.unzip_file(zip_path, extract_dir) # Verify the root file was extracted @@ -145,7 +145,7 @@ defmodule Pleroma.SafeZipTest do test "can create zip with directories" do # Create a directory structure dir_path = Path.join(@tmp_dir, "test_dir") - File.mkdir_p!(dir_path) + Pleroma.Backports.mkdir_p!(dir_path) file_in_dir_path = Path.join(dir_path, "file_in_dir.txt") File.write!(file_in_dir_path, "file in directory") @@ -428,7 +428,7 @@ defmodule Pleroma.SafeZipTest do # Create a directory and a file in it dir_path = Path.join(@tmp_dir, "file_in_dir") - File.mkdir_p!(dir_path) + Pleroma.Backports.mkdir_p!(dir_path) file_in_dir_path = Path.join(dir_path, "test_file.txt") File.write!(file_in_dir_path, "file in directory content") diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 176e70ef9..0b4dc9197 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -2669,8 +2669,12 @@ defmodule Pleroma.UserTest do assert {:ok, user} = User.update_last_active_at(user) - assert user.last_active_at >= test_started_at - assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq] + + assert NaiveDateTime.compare( + user.last_active_at, + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + ) in [:lt, :eq] last_active_at = NaiveDateTime.utc_now() @@ -2682,10 +2686,15 @@ defmodule Pleroma.UserTest do |> cast(%{last_active_at: last_active_at}, [:last_active_at]) |> User.update_and_set_cache() - assert user.last_active_at == last_active_at + assert NaiveDateTime.compare(user.last_active_at, last_active_at) == :eq + assert {:ok, user} = User.update_last_active_at(user) - assert user.last_active_at >= test_started_at - assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq] + + assert NaiveDateTime.compare( + user.last_active_at, + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + ) in [:lt, :eq] end test "active_user_count/1" do @@ -2783,6 +2792,15 @@ defmodule Pleroma.UserTest do assert user_updated.also_known_as |> length() == 1 assert user2.ap_id in user_updated.also_known_as end + + test "should tolerate non-http(s) aliases" do + user = + insert(:user, %{ + also_known_as: ["at://did:plc:xgvzy7ni6ig6ievcbls5jaxe"] + }) + + assert "at://did:plc:xgvzy7ni6ig6ievcbls5jaxe" in user.also_known_as + end end describe "alias_users/1" do diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index dbc3aa532..c16f081f6 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -826,7 +826,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert object.data["repliesCount"] == 2 end - test "increates quotes count", %{user: user} do + test "increases quotes count", %{user: user} do user2 = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"}) diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index 829598246..3c7ff0eeb 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -5,12 +5,33 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest do use Pleroma.DataCase, async: true + alias Pleroma.Language.LanguageDetectorMock + alias Pleroma.StaticStubbedConfigMock alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator alias Pleroma.Web.ActivityPub.Utils + import Mox import Pleroma.Factory + # Setup for all tests + setup do + # Stub the StaticStubbedConfigMock to return our mock for the provider + StaticStubbedConfigMock + |> stub(:get, fn + [Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock + _other -> nil + end) + + # Stub the LanguageDetectorMock with default implementations + LanguageDetectorMock + |> stub(:missing_dependencies, fn -> [] end) + |> stub(:configured?, fn -> true end) + |> stub(:detect, fn _text -> nil end) + + :ok + end + describe "Notes" do setup do user = insert(:user) @@ -234,6 +255,37 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest assert object.language == "pl" end + test "it doesn't call LanguageDetector when language is specified" do + # Set up expectation that detect should not be called + LanguageDetectorMock + |> expect(:detect, 0, fn _ -> flunk("LanguageDetector.detect should not be called") end) + |> stub(:missing_dependencies, fn -> [] end) + |> stub(:configured?, fn -> true end) + + # Stub the StaticStubbedConfigMock to return our mock for the provider + StaticStubbedConfigMock + |> stub(:get, fn + [Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock + _other -> nil + end) + + user = insert(:user) + + note = %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "id" => Utils.generate_object_id(), + "type" => "Note", + "content" => "a post in English", + "contentMap" => %{ + "en" => "a post in English" + }, + "attributedTo" => user.ap_id + } + + ArticleNotePageValidator.cast_and_apply(note) + end + test "it adds contentMap if language is specified" do user = insert(:user) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 99ed42877..b7ff0ed5f 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -520,4 +520,105 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert decoded["cc"] == [] end + + test "unlisted activities retain public address in cc" do + user = insert(:user) + + # simulate unlistd activity by only having + # public address in cc + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => [@as_public], + "to" => [user.follower_address] + } + ) + + assert @as_public in activity.data["cc"] + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert @as_public in decoded["cc"] + + # maybe we also have another inbox in cc + # during Publishing + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => [@as_public], + "to" => [user.follower_address] + } + ) + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: ["https://remote.instance/users/someone_else/inbox"] + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert decoded["cc"] == [@as_public, "https://remote.instance/users/someone_else/inbox"] + end + + test "public address in cc parameter is preserved" do + user = insert(:user) + + cc_with_public = [@as_public, "https://example.org/users/other"] + + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => cc_with_public, + "to" => [user.follower_address] + } + ) + + assert @as_public in activity.data["cc"] + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: cc_with_public + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert cc_with_public == decoded["cc"] + end + + test "cc parameter is preserved" do + user = insert(:user) + + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => ["https://example.com/specific/user"], + "to" => [user.follower_address] + } + ) + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: ["https://example.com/specific/user"] + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert decoded["cc"] == ["https://example.com/specific/user"] + end end diff --git a/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs index c02f66d77..27f8522ce 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/like_handling_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do use Pleroma.DataCase, async: true alias Pleroma.Activity + alias Pleroma.Object alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.CommonAPI @@ -75,4 +76,107 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do assert activity_data["object"] == activity.data["object"] assert activity_data["content"] == "⭐" end + + test "it works for misskey likes with custom emoji" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/misskey-custom-emoji-like.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) + + assert activity_data["actor"] == data["actor"] + assert activity_data["type"] == "EmojiReact" + assert activity_data["id"] == data["id"] + assert activity_data["object"] == activity.data["object"] + assert activity_data["content"] == ":blobwtfnotlikethis:" + + assert [["blobwtfnotlikethis", _, _]] = + Object.get_by_ap_id(activity.data["object"]) + |> Object.get_emoji_reactions() + end + + test "it works for mitra likes with custom emoji" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/mitra-custom-emoji-like.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) + + assert activity_data["actor"] == data["actor"] + assert activity_data["type"] == "EmojiReact" + assert activity_data["id"] == data["id"] + assert activity_data["object"] == activity.data["object"] + assert activity_data["content"] == ":ablobcatheartsqueeze:" + + assert [["ablobcatheartsqueeze", _, _]] = + Object.get_by_ap_id(activity.data["object"]) + |> Object.get_emoji_reactions() + end + + test "it works for likes with wrong content" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/mitra-custom-emoji-like.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("content", 1) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + assert {:ok, activity} = Transmogrifier.handle_incoming(data) + assert activity.data["type"] == "Like" + end + + test "it changes incoming dislikes into emoji reactions" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/friendica-dislike.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + refute Enum.empty?(activity.recipients) + + assert data["actor"] == "https://my-place.social/profile/vaartis" + assert data["type"] == "EmojiReact" + assert data["content"] == "👎" + assert data["id"] == "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182" + assert data["object"] == activity.data["object"] + + data = + File.read!("test/fixtures/friendica-dislike-undo.json") + |> Jason.decode!() + |> put_in(["object", "object"], activity.data["object"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "https://my-place.social/profile/vaartis" + assert data["type"] == "Undo" + + assert data["object"] == + "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182" + end end diff --git a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs index 0d1a4999e..a6b8dba46 100644 --- a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs @@ -13,7 +13,7 @@ defmodule Pleroma.Web.AdminAPI.FrontendControllerTest do setup do clear_config([:instance, :static_dir], @dir) - File.mkdir_p!(Pleroma.Frontend.dir()) + Pleroma.Backports.mkdir_p!(Pleroma.Frontend.dir()) on_exit(fn -> File.rm_rf(@dir) diff --git a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs index 9511dccea..344c908fe 100644 --- a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do @default_instance_panel ~s(

Welcome to Pleroma!

) setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) on_exit(fn -> File.rm_rf(@dir) end) end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index 73230a58c..6b5d31537 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -111,6 +111,17 @@ defmodule Pleroma.Web.CommonAPITest do end end + test "add expiring block", %{blocker: blocker, blocked: blocked} do + {:ok, _} = CommonAPI.block(blocked, blocker, %{expires_in: 60}) + assert User.blocks?(blocker, blocked) + + worker = Pleroma.Workers.MuteExpireWorker + args = %{"op" => "unblock_user", "blocker_id" => blocker.id, "blocked_id" => blocked.id} + + assert :ok = perform_job(worker, args) + refute User.blocks?(blocker, blocked) + end + test "it blocks and does not federate if outgoing blocks are disabled", %{ blocker: blocker, blocked: blocked diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs index 373a84303..8a0fe5259 100644 --- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs @@ -152,4 +152,46 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do } ] = result["rules"] end + + test "translation languages matrix", %{conn: conn} do + clear_config([Pleroma.Language.Translation, :provider], TranslationMock) + + assert %{"en" => ["pl"], "pl" => ["en"]} = + conn + |> get("/api/v1/instance/translation_languages") + |> json_response_and_validate_schema(200) + end + + test "base_urls in pleroma metadata", %{conn: conn} do + media_proxy_base_url = "https://media.example.org" + upload_base_url = "https://uploads.example.org" + + clear_config([:media_proxy, :enabled], true) + clear_config([:media_proxy, :base_url], media_proxy_base_url) + clear_config([Pleroma.Upload, :base_url], upload_base_url) + + conn = get(conn, "/api/v1/instance") + + assert result = json_response_and_validate_schema(conn, 200) + assert result["pleroma"]["metadata"]["base_urls"]["media_proxy"] == media_proxy_base_url + assert result["pleroma"]["metadata"]["base_urls"]["upload"] == upload_base_url + + # Test when media_proxy is disabled + clear_config([:media_proxy, :enabled], false) + + conn = get(conn, "/api/v1/instance") + + assert result = json_response_and_validate_schema(conn, 200) + refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "media_proxy") + assert result["pleroma"]["metadata"]["base_urls"]["upload"] == upload_base_url + + # Test when upload base_url is not set + clear_config([Pleroma.Upload, :base_url], nil) + + conn = get(conn, "/api/v1/instance") + + assert result = json_response_and_validate_schema(conn, 200) + refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "media_proxy") + refute Map.has_key?(result["pleroma"]["metadata"]["base_urls"], "upload") + end end diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index a4bca6cf9..25a17d5c1 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -2483,4 +2483,62 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> json_response_and_validate_schema(:not_found) end end + + describe "translating statuses" do + setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock) + + test "it translates a status to user language" do + user = insert(:user, language: "fr") + %{conn: conn} = oauth_access(["read:statuses"], user: user) + another_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(another_user, %{ + status: "Cześć!", + visibility: "public", + language: "pl" + }) + + response = + conn + |> post("/api/v1/statuses/#{activity.id}/translate") + |> json_response_and_validate_schema(200) + + assert response == %{ + "content" => "!ćśezC", + "detected_source_language" => "pl", + "provider" => "TranslationMock" + } + end + + test "it returns an error if no target language provided" do + %{conn: conn} = oauth_access(["read:statuses"]) + another_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(another_user, %{ + status: "Cześć!", + language: "pl" + }) + + assert conn + |> post("/api/v1/statuses/#{activity.id}/translate") + |> json_response_and_validate_schema(400) + end + + test "it doesn't translate non-public statuses" do + %{conn: conn, user: user} = oauth_access(["read:statuses"]) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "Cześć!", + visibility: "private", + language: "pl" + }) + + assert conn + |> post("/api/v1/statuses/#{activity.id}/translate") + |> json_response_and_validate_schema(404) + end + end end diff --git a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs index be94a02ad..bcc25b83e 100644 --- a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs @@ -19,10 +19,33 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do "artist" => "lain", "album" => "lain radio", "length" => "180000", - "externalLink" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" + "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" }) - assert %{"title" => "lain radio episode 1"} = json_response_and_validate_schema(conn, 200) + assert %{ + "title" => "lain radio episode 1", + "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" + } = json_response_and_validate_schema(conn, 200) + end + + test "external_link fallback" do + %{conn: conn} = oauth_access(["write"]) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/scrobble", %{ + "title" => "lain radio episode 2", + "artist" => "lain", + "album" => "lain radio", + "length" => "180000", + "externalLink" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" + }) + + assert %{ + "title" => "lain radio episode 2", + "external_link" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" + } = json_response_and_validate_schema(conn, 200) end end @@ -35,7 +58,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do title: "lain radio episode 1", artist: "lain", album: "lain radio", - externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" + external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" }) {:ok, _activity} = @@ -43,7 +66,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do title: "lain radio episode 2", artist: "lain", album: "lain radio", - externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" + external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" }) {:ok, _activity} = @@ -51,7 +74,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do title: "lain radio episode 3", artist: "lain", album: "lain radio", - externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+3" + external_link: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+3" }) conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles") diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs index 6f4d24d9e..a7af3e74e 100644 --- a/test/pleroma/web/plugs/frontend_static_plug_test.exs +++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs @@ -13,7 +13,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do @dir "test/tmp/instance_static" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) on_exit(fn -> File.rm_rf(@dir) end) end @@ -38,7 +38,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) File.write!("#{path}/index.html", "from frontend plug") index = get(conn, "/") @@ -52,7 +52,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do clear_config([:frontends, :admin], %{"name" => name, "ref" => ref}) path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) File.write!("#{path}/index.html", "from frontend plug") index = get(conn, "/pleroma/admin/") @@ -67,7 +67,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do clear_config([:frontends, :primary], %{"name" => name, "ref" => ref}) path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!("#{path}/proxy/rr/ss") + Pleroma.Backports.mkdir_p!("#{path}/proxy/rr/ss") File.write!("#{path}/proxy/rr/ss/Ek7w8WPVcAApOvN.jpg:large", "FB image") ConfigMock diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs index 33b74dcf0..b5a5a3334 100644 --- a/test/pleroma/web/plugs/instance_static_test.exs +++ b/test/pleroma/web/plugs/instance_static_test.exs @@ -8,7 +8,7 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do @dir "test/tmp/instance_static" setup do - File.mkdir_p!(@dir) + Pleroma.Backports.mkdir_p!(@dir) on_exit(fn -> File.rm_rf(@dir) end) end @@ -34,7 +34,7 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do refute html_response(bundled_index, 200) == "from frontend plug" path = "#{@dir}/frontends/#{name}/#{ref}" - File.mkdir_p!(path) + Pleroma.Backports.mkdir_p!(path) File.write!("#{path}/index.html", "from frontend plug") index = get(conn, "/") diff --git a/test/pleroma/web/plugs/user_tracking_plug_test.exs b/test/pleroma/web/plugs/user_tracking_plug_test.exs index 742f04fea..cd9c66448 100644 --- a/test/pleroma/web/plugs/user_tracking_plug_test.exs +++ b/test/pleroma/web/plugs/user_tracking_plug_test.exs @@ -21,8 +21,12 @@ defmodule Pleroma.Web.Plugs.UserTrackingPlugTest do |> assign(:user, user) |> UserTrackingPlug.call(%{}) - assert user.last_active_at >= test_started_at - assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq] + + assert NaiveDateTime.compare( + user.last_active_at, + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + ) in [:lt, :eq] end test "doesn't update last_active_at if it was updated recently", %{conn: conn} do @@ -38,7 +42,7 @@ defmodule Pleroma.Web.Plugs.UserTrackingPlugTest do |> assign(:user, user) |> UserTrackingPlug.call(%{}) - assert user.last_active_at == last_active_at + assert NaiveDateTime.compare(user.last_active_at, last_active_at) == :eq end test "skips updating last_active_at if user ID is nil", %{conn: conn} do diff --git a/test/pleroma/web/rich_media/parser_test.exs b/test/pleroma/web/rich_media/parser_test.exs index 20f61badc..1f01d657a 100644 --- a/test/pleroma/web/rich_media/parser_test.exs +++ b/test/pleroma/web/rich_media/parser_test.exs @@ -61,6 +61,13 @@ defmodule Pleroma.Web.RichMedia.ParserTest do }} end + test "truncates title and description fields" do + {:ok, parsed} = Parser.parse("https://instagram.com/longtext") + + assert String.length(parsed["title"]) == 120 + assert String.length(parsed["description"]) == 200 + end + test "parses OEmbed and filters HTML tags" do assert Parser.parse("https://example.com/oembed") == {:ok, diff --git a/test/pleroma/web/web_finger/web_finger_controller_test.exs b/test/pleroma/web/web_finger/web_finger_controller_test.exs index 80e072163..be44e3a8b 100644 --- a/test/pleroma/web/web_finger/web_finger_controller_test.exs +++ b/test/pleroma/web/web_finger/web_finger_controller_test.exs @@ -5,7 +5,6 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do use Pleroma.Web.ConnCase - import ExUnit.CaptureLog import Pleroma.Factory import Tesla.Mock @@ -55,6 +54,26 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do ] end + test "Webfinger defaults to JSON when no Accept header is provided" do + user = + insert(:user, + ap_id: "https://hyrule.world/users/zelda", + also_known_as: ["https://mushroom.kingdom/users/toad"] + ) + + response = + build_conn() + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + |> json_response(200) + + assert response["subject"] == "acct:#{user.nickname}@localhost" + + assert response["aliases"] == [ + "https://hyrule.world/users/zelda", + "https://mushroom.kingdom/users/toad" + ] + end + test "reach user on tld, while pleroma is running on subdomain" do clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com") @@ -109,16 +128,25 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do assert result == "Couldn't find user" end - test "Sends a 404 when invalid format" do - user = insert(:user) + test "Returns JSON when format is not supported" do + user = + insert(:user, + ap_id: "https://hyrule.world/users/zelda", + also_known_as: ["https://mushroom.kingdom/users/toad"] + ) - assert capture_log(fn -> - assert_raise Phoenix.NotAcceptableError, fn -> - build_conn() - |> put_req_header("accept", "text/html") - |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") - end - end) =~ "no supported media type in accept header" + response = + build_conn() + |> put_req_header("accept", "text/html") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + |> json_response(200) + + assert response["subject"] == "acct:#{user.nickname}@localhost" + + assert response["aliases"] == [ + "https://hyrule.world/users/zelda", + "https://mushroom.kingdom/users/toad" + ] end test "Sends a 400 when resource param is missing" do diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 1c472fca9..f8d11e602 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1494,6 +1494,11 @@ defmodule HttpRequestMock do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}} end + def get("https://instagram.com/longtext", _, _, _) do + {:ok, + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/instagram_longtext.html")}} + end + def get("https://example.com/non-ogp", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")}} @@ -1701,6 +1706,24 @@ defmodule HttpRequestMock do }} end + def post("https://api-free.deepl.com/v2/translate" <> _, _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/deepl-translation.json"), + headers: [{"content-type", "application/json"}] + }} + end + + def post("https://api-free.deepl.com/v2/languages" <> _, _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/deepl-languages-list.json"), + headers: [{"content-type", "application/json"}] + }} + end + def post(url, query, body, headers) do {:error, "Mock response not implemented for POST #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"} @@ -1720,7 +1743,8 @@ defmodule HttpRequestMock do "https://example.com/twitter-card", "https://google.com/", "https://pleroma.local/notice/9kCP7V", - "https://yahoo.com/" + "https://yahoo.com/", + "https://instagram.com/longtext" ] def head(url, _query, _body, _headers) when url in @rich_media_mocks do diff --git a/test/support/mocks.ex b/test/support/mocks.ex index ca2974504..b26834871 100644 --- a/test/support/mocks.ex +++ b/test/support/mocks.ex @@ -34,5 +34,9 @@ Mox.defmock(Pleroma.LoggerMock, for: Pleroma.Logging) Mox.defmock(Pleroma.Uploaders.S3.ExAwsMock, for: Pleroma.Uploaders.S3.ExAwsAPI) +Mox.defmock(Pleroma.Language.LanguageDetectorMock, + for: Pleroma.Language.LanguageDetector.Provider +) + Mox.defmock(Pleroma.DateTimeMock, for: Pleroma.DateTime) Mox.defmock(Pleroma.MogrifyMock, for: Pleroma.MogrifyBehaviour) diff --git a/test/support/translation_mock.ex b/test/support/translation_mock.ex new file mode 100644 index 000000000..84ed8f696 --- /dev/null +++ b/test/support/translation_mock.ex @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule TranslationMock do + alias Pleroma.Language.Translation.Provider + + use Provider + + @behaviour Provider + + @name "TranslationMock" + + @impl Provider + def configured?, do: true + + @impl Provider + def translate(content, source_language, _target_language) do + {:ok, + %{ + content: content |> String.reverse(), + detected_source_language: source_language, + provider: @name + }} + end + + @impl Provider + def supported_languages(_) do + {:ok, ["en", "pl"]} + end + + @impl Provider + def languages_matrix do + {:ok, + %{ + "en" => ["pl"], + "pl" => ["en"] + }} + end + + @impl Provider + def name, do: @name +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 94661353b..dc6c05a74 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -2,8 +2,6 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -Code.put_compiler_option(:warnings_as_errors, true) - ExUnit.configure(capture_log: true, max_cases: System.schedulers_online()) ExUnit.start(exclude: [:federated])