Merge remote-tracking branch 'origin/develop' into shigusegubu

This commit is contained in:
Henry Jameson 2025-06-25 14:23:41 +03:00
commit 63b82091d9
153 changed files with 3143 additions and 539 deletions

View file

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

View file

@ -0,0 +1 @@
Add new activity actor/type index. Greatly speeds up retrieval of rare types (like "Listen")

View file

@ -0,0 +1 @@
Fix 'Create a user' description in admin api docs

View file

View file

@ -0,0 +1 @@
Fix AssignAppUser migration OOM

View file

@ -0,0 +1 @@
Use JSON for DeepL API requests

View file

@ -0,0 +1 @@
Support Dislike activity, as sent by Mitra and Friendica, by changing it into a thumbs-down EmojiReact

View file

View file

@ -0,0 +1 @@
Elixir 1.18: Fixed warnings and new deprecations

View file

@ -0,0 +1 @@
Support Mitra-style emoji likes.

View file

@ -0,0 +1 @@
Add `duration` to the block endpoint, which makes block expire

View file

@ -0,0 +1 @@
Expose markup configuration in InstanceView

View file

View file

@ -0,0 +1 @@
Set PATH in the FreeBSD rc script to avoid failures starting the service

View file

@ -0,0 +1 @@
Improved performance of status search queries using the default GIN index

View file

@ -0,0 +1 @@
Implement language detection with fastText

View file

View file

@ -0,0 +1 @@
Updated relayd/httpd config files to be on par with nginx

View file

@ -0,0 +1 @@
replaced depracated flags and functions, renamed service to fit other service files

View file

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

View file

View file

@ -0,0 +1 @@
Relax alsoKnownAs requirements to just URI, not necessarily HTTP(S)

1
changelog.d/releases.fix Normal file
View file

@ -0,0 +1 @@
Fix release builds

View file

@ -0,0 +1 @@
Change scrobble external link param name to use snake case

View file

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

View file

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

View file

@ -0,0 +1 @@
Allow Terms of Service panel behaviour to be configurable

View file

@ -0,0 +1 @@
Support translation providers (DeepL, LibreTranslate)

View file

@ -0,0 +1 @@
Truncate the length of Rich Media title and description fields

0
changelog.d/typos.skip Normal file
View file

View file

@ -0,0 +1 @@
Don't require an Accept header for WebFinger queries and default to JSON.

View file

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

View file

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

View file

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

View file

@ -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"]
}
]
}
]

View file

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

View file

@ -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: Users nickname
- Response: Array of user objects
## `POST /api/v1/pleroma/admin/users/follow`

View file

@ -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"
}
]

View file

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

View file

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

View file

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

View file

@ -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`.<br>
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`.<br>
Changing the password hashing algorithm is not needed.<br>
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 <path>` and set the user to postgres with the `-U <username>` 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:
* `<ipaddr>` 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="<ip6addr>"` line near the beginning of the file and change `<ip6addr` to your instance's IPv6 address
- Uncomment the line starting with `listen on $ext_inet6` in the `server` block
Check the configuration by running:
```
# $OpenBSD: httpd.conf,v 1.17 2017/04/16 08:50:49 ajacoutot Exp $
ext_inet="<IPv4 address>"
ext_inet6="<IPv6 address>"
server "default" {
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 127.0.0.1 port 80 # Do NOT comment this line
log syslog
directory no index
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location "/robots.txt" { root "/htdocs/local/" }
location "/*" { block return 302 "https://$HTTP_HOST$REQUEST_URI" }
}
types {
}
```
Do not forget to change *<IPv4/6 address\>* 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-<domain name> {
#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-<domain name>.pem"
}
```
# rcctl enable httpd
# rcctl start httpd
```
domain <domain name> {
domain key "/etc/ssl/private/<domain name>.key"
domain certificate "/etc/ssl/<domain name>.crt"
domain full chain certificate "/etc/ssl/<domain name>.fullchain.pem"
sign with letsencrypt-<domain name>
challengedir "/var/www/acme/"
}
```
Replace *<domain name\>* by the domain name you'll use for your instance. As root, run `acme-client -n` to check the config, then `acme-client -ADv <domain name>` 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 <domain name>" >> /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/<domain name>.fullchain.pem /etc/ssl/<IP address>.crt
ln -s /etc/ssl/private/<domain name>.key /etc/ssl/private/<IP address>.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:
* `<ipaddr>` 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="<ip6addr>"` line near the beginning of the file and change `<ip6addr>` 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="<IPv4 address>"
ext_inet6="<IPv6 address>"
table <pleroma_server> { 127.0.0.1 }
table <httpd_server> { 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 <httpd_server>
# 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 <pleroma_server> port 4000 check http "/" code 200
forward to <httpd_server> port 80 check http "/robots.txt" code 200
}
```
Again, change *<IPv4/6 address\>* to your server's address(es) and comment one of the two *listen* options if needed. Also change *wss://CHANGEME.tld* to *wss://<your instance's domain name\>*.
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="<network interface>"
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 *<network interface\>* 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 <username> <your@emailaddress> --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 <username> <your@emailaddress> --admin
```
### Further reading
{! backend/installation/further_reading.include !}

View file

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

View file

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

View file

@ -2,20 +2,21 @@
# Default httpd.conf file for Pleroma on OpenBSD
# Simple installation instructions
# 1. Place file in /etc
# 2. Replace <IPv4 address> with your public IP address
# 3. If using IPv6, uncomment IPv6 lines and replace <IPv6 address> with your public IPv6 address
# 4. Check file using 'doas httpd -n'
# 5. Enable and start httpd:
# 2. Replace <ipaddr> with your public IP address
# 3. If using IPv6, uncomment IPv6 lines and replace <ip6addr> 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="<IPv4 address>"
#ext_inet6="<IPv6 address>"
ext_inet="<ipaddr>"
#ext_inet6="<ip6addr>"
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"
}

View file

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

View file

@ -3,9 +3,10 @@
# Simple installation instructions:
# 1. Place in /etc
# 2. Replace <ipaddr> with your public IPv4 address
# 3. If using IPv6i, uncomment IPv6 lines and replace <ip6addr> 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 <ip6addr> 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="<ipaddr>"
#ext_inet6="<ip6addr>"
table <pleroma_server> { 127.0.0.1 }
table <httpd_server> { 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 <httpd_server> { 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 <httpd_server>
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 <pleroma_server>
# 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 <pleroma_server>
# When serving multiple services, add the forwards here.
# Example:
#pass request quick header "Host" value "example.tld" forward to <httpd_server>
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 <pleroma_server> port 4000 check http "/" code 200
forward to <httpd_server> port 80 check http "/robots.txt" code 200
forward to <pleroma_server> 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 <httpd_server> port 8080
}

View file

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

View file

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

View file

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

View file

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

View file

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

72
lib/pleroma/backports.ex Normal file
View file

@ -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]
# <https://github.com/elixir-lang/elixir/pull/14242>
# 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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,47 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,11 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,127 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,121 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,93 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -0,0 +1,40 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -1,28 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# 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

View file

@ -102,7 +102,8 @@ defmodule Pleroma.Search.DatabaseSearch do
^tsc,
o.data,
^search_query
)
),
order_by: [desc: :inserted_at]
)
end

View file

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

View file

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

View file

@ -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)},

View file

@ -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()}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"],

View file

@ -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")],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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