diff --git a/.gitignore b/.gitignore
index 355cea069..d8e5ed553 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,6 +56,9 @@ pleroma.iml
# asdf
.tool-versions
+# mise
+mise.toml
+
# Editor temp files
*~
*#
diff --git a/.woodpecker/changelog.yaml b/.woodpecker/changelog.yaml
new file mode 100644
index 000000000..64062f17e
--- /dev/null
+++ b/.woodpecker/changelog.yaml
@@ -0,0 +1,19 @@
+when:
+ - event: pull_request
+
+labels:
+ platform: linux/amd64
+
+variables:
+ script_file_entrypoint: &script_file_entrypoint
+ - /bin/sh
+ - -c
+ - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
+
+steps:
+ check-changelog:
+ image: docker.io/alpine:3.23
+ entrypoint: *script_file_entrypoint
+ commands:
+ - apk add --no-cache git
+ - sh ./tools/check-changelog
diff --git a/.woodpecker/docker-combine.yaml b/.woodpecker/docker-combine.yaml
new file mode 100644
index 000000000..6c91d26e9
--- /dev/null
+++ b/.woodpecker/docker-combine.yaml
@@ -0,0 +1,60 @@
+when:
+ - event: push
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ]
+ - event: tag
+ - event: manual
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ - event: manual
+ branch: stable
+
+depends_on:
+ - docker
+
+skip_clone: true
+
+labels:
+ platform: linux/amd64
+
+steps:
+ docker-develop-combine:
+ image: git.fluffytail.org/phnt/wpc-docker-tagger:latest
+ when:
+ - event: push
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ - event: manual
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ settings: &docker_settings
+ registry: "git.pleroma.social"
+ image: "pleroma/pleroma"
+ architectures: [amd64, arm64]
+ tags:
+ - latest
+ - develop
+ - ${CI_COMMIT_SHA:0:8}
+ username:
+ from_secret: pleroma-ci-user
+ password:
+ from_secret: pleroma-ci-password
+
+ docker-stable-combine:
+ image: git.fluffytail.org/phnt/wpc-docker-tagger:latest
+ when:
+ - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""'
+ settings:
+ <<: *docker_settings
+ tags: &stable_docker_tags
+ - latest
+ - stable
+ - ${CI_COMMIT_SHA:0:8}
+
+ docker-stable-tag-combine:
+ image: git.fluffytail.org/phnt/wpc-docker-tagger:latest
+ when:
+ - event: tag
+ - evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""'
+ settings:
+ <<: *docker_settings
+ tags:
+ - <<: *stable_docker_tags
+ - ${CI_COMMIT_TAG}
diff --git a/.woodpecker/docker.yaml b/.woodpecker/docker.yaml
new file mode 100644
index 000000000..abc6bfa3b
--- /dev/null
+++ b/.woodpecker/docker.yaml
@@ -0,0 +1,96 @@
+when:
+ - event: push
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ]
+ - event: tag
+ - event: manual
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ - event: manual
+ branch: stable
+
+matrix:
+ platform:
+ - linux/amd64
+ - linux/arm64
+
+# This is needed for the when clauses below.
+labels:
+ platform: ${platform}
+ memory: 'high'
+
+variables:
+ docker_variables: &docker_variables
+ repo: pleroma/pleroma
+ registry: git.pleroma.social
+ username:
+ from_secret: pleroma-ci-user
+ password:
+ from_secret: pleroma-ci-password
+ kaniko_image: &kaniko_image woodpeckerci/plugin-kaniko:2.3.1
+
+steps:
+ docker-develop-amd64:
+ image: woodpeckerci/plugin-kaniko:2.3.1
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ settings:
+ <<: *docker_variables
+ tags:
+ - latest-amd64
+ - develop-amd64
+ - ${CI_COMMIT_SHA:0:8}-amd64
+
+ docker-develop-arm64:
+ image: woodpeckerci/plugin-kaniko:2.3.1
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ settings:
+ <<: *docker_variables
+ tags:
+ - latest-arm64
+ - develop-arm64
+ - ${CI_COMMIT_SHA:0:8}-arm64
+
+ docker-stable-amd64:
+ image: *kaniko_image
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""'
+ settings:
+ <<: *docker_variables
+ tags: &amd64_tags
+ - latest-amd64
+ - stable-amd64
+ - ${CI_COMMIT_SHA:0:8}-amd64
+
+ docker-stable-tag-amd64:
+ image: *kaniko_image
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""'
+ settings:
+ <<: *docker_variables
+ tags:
+ - <<: *amd64_tags
+ - ${CI_COMMIT_TAG}-amd64
+
+ docker-stable-arm64:
+ image: *kaniko_image
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""'
+ settings:
+ <<: *docker_variables
+ tags: &arm64_tags
+ - latest-arm64
+ - stable-arm64
+ - ${CI_COMMIT_SHA:0:8}-arm64
+
+ docker-stable-tag-arm64:
+ image: *kaniko_image
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""'
+ settings:
+ <<: *docker_variables
+ tags:
+ - <<: *arm64_tags
+ - ${CI_COMMIT_TAG}-arm64
diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml
new file mode 100644
index 000000000..0ab7441a8
--- /dev/null
+++ b/.woodpecker/lint.yaml
@@ -0,0 +1,77 @@
+when:
+ - event: pull_request
+ path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
+ - event: push
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
+
+labels:
+ platform: linux/amd64
+
+variables:
+ script_file_entrypoint: &script_file_entrypoint
+ - /bin/sh
+ - -c
+ - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
+
+steps:
+ mix-format:
+ image: &elixir-image
+ docker.io/elixir:1.15-alpine
+ entrypoint: *script_file_entrypoint
+ failure: ignore
+ commands:
+ - |
+ if ! mix format --check-formatted; then
+ touch fail.stamp
+ exit 1
+ fi
+
+ credo:
+ image: *elixir-image
+ entrypoint: *script_file_entrypoint
+ failure: ignore
+ environment:
+ MIX_ENV: test
+ commands:
+ - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl
+ - adduser -D -h /home/testuser testuser
+ - mkdir -p /home/testuser/.mix /home/testuser/.hex
+ - chown -R testuser:testuser . /home/testuser
+ - su testuser -c "HOME=/home/testuser mix local.hex --force"
+ - su testuser -c "HOME=/home/testuser mix local.rebar --force"
+ - su testuser -c "HOME=/home/testuser mix deps.get"
+ - |
+ if ! su testuser -c "HOME=/home/testuser mix analyze"; then
+ touch fail.stamp
+ exit 1
+ fi
+
+ # cycles:
+ # image: *elixir-image
+ # failure: ignore
+ # commands:
+ # - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl
+ # - adduser -D -h /home/testuser testuser
+ # - mkdir -p /home/testuser/.mix /home/testuser/.hex
+ # - chown -R testuser:testuser . /home/testuser
+ # - su testuser -c "HOME=/home/testuser mix local.hex --force"
+ # - su testuser -c "HOME=/home/testuser mix local.rebar --force"
+ # - su testuser -c "HOME=/home/testuser mix compile"
+ # - |
+ # if ! su testuser -c "HOME=/home/testuser mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != \"No cycles found\")}'"; then
+ # touch fail.stamp
+ # exit 1
+ # fi
+
+ ensure-status:
+ image: *elixir-image
+ entrypoint: *script_file_entrypoint
+ commands: |
+ if test -f fail.stamp; then
+ echo "One or more previous steps fails. Failing workflow..."
+ exit 1
+ else
+ echo "All steps passed"
+ exit 0
+ fi
diff --git a/.woodpecker/otp-musl.yaml b/.woodpecker/otp-musl.yaml
new file mode 100644
index 000000000..60558b893
--- /dev/null
+++ b/.woodpecker/otp-musl.yaml
@@ -0,0 +1,292 @@
+when:
+ - event: push
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
+ - event: tag
+ - event: manual
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ - event: manual
+ branch: stable
+
+matrix:
+ platform:
+ - linux/amd64
+ - linux/arm
+ - linux/arm64
+
+# This is needed for the when clauses below.
+# When the platform clause is fixed, this might not be needed anymore
+labels:
+ platform: ${platform}
+
+variables:
+ script_file_entrypoint: &script_file_entrypoint
+ - /bin/sh
+ - -c
+ - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
+ build_cmds: &build_cmds
+ - apk add git build-base cmake file-dev openssl vips-dev zip
+ - echo "import Config" > config/prod.secret.exs
+ - mix local.hex --force
+ - mix local.rebar --force
+ - mix deps.get --only prod
+ - mkdir release
+ - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH}
+ - mix release --path release
+ build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1
+ build_image_arm: &build_image_arm docker.io/arm32v7/elixir:1.17.3-alpine
+ build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1
+ artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0
+ artifacts_uploader_settings: &artifacts_uploader_settings
+ user:
+ from_secret: pleroma-ci-user
+ password:
+ from_secret: pleroma-ci-password
+ owner: 'pleroma'
+ env: &env
+ MIX_ENV: prod
+ VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS
+
+steps:
+ otp-develop-amd64-musl:
+ image: *build_image_amd64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ environment: *env
+ commands: &amd64_build
+ - <<: *build_cmds
+ - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release
+
+ otp-stable-amd64-musl:
+ image: *build_image_amd64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
+ environment: *env
+ commands: *amd64_build
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ otp-stable-tag-amd64-musl:
+ image: *build_image_amd64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
+ environment: *env
+ commands:
+ - <<: *build_cmds
+ - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release
+
+ otp-develop-arm-musl:
+ image: *build_image_arm
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ environment: *env
+ commands: &arm_build
+ - <<: *build_cmds
+ - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip release
+
+ otp-stable-arm-musl:
+ image: *build_image_arm
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
+ environment: *env
+ commands: *arm_build
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ otp-stable-tag-arm-musl:
+ image: *build_image_arm
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
+ environment: *env
+ commands:
+ - <<: *build_cmds
+ - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip release
+
+ otp-develop-arm64-musl:
+ image: *build_image_arm64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ environment: *env
+ commands: &arm64_build
+ - <<: *build_cmds
+ - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release
+
+ otp-stable-arm64-musl:
+ image: *build_image_arm64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
+ environment: *env
+ commands: *arm64_build
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ otp-stable-tag-arm64-musl:
+ image: *build_image_arm64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
+ environment: *env
+ commands:
+ - <<: *build_cmds
+ - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release
+
+ upload-artifacts-amd64-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl
+ package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
+ file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
+ update: 'true'
+
+ upload-latest-amd64-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl
+ package_version: latest
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
+ file_name: pleroma.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-artifacts-tag-amd64-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-amd64-musl
+ package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
+ file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-latest-tag-amd64-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-amd64-musl
+ package_version: latest
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
+ file_name: pleroma.zip
+ update: 'true'
+
+ upload-artifacts-arm-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm-musl
+ package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip
+ file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip
+ update: 'true'
+
+ upload-latest-arm-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm-musl
+ package_version: latest
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip
+ file_name: pleroma.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-artifacts-tag-arm-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-arm-musl
+ package_version: stable-${CI_COMMIT_SHA:0:8}-arm-musl
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip
+ file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-latest-tag-arm-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-arm-musl
+ package_version: latest
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip
+ file_name: pleroma.zip
+ update: 'true'
+
+ upload-artifacts-arm64-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl
+ package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
+ file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
+ update: 'true'
+
+ upload-latest-arm64-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl
+ package_version: latest
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
+ file_name: pleroma.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-artifacts-tag-arm64-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-arm64-musl
+ package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
+ file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-latest-tag-arm64-musl:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-arm64-musl
+ package_version: latest
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
+ file_name: pleroma.zip
+ update: 'true'
diff --git a/.woodpecker/otp.yaml b/.woodpecker/otp.yaml
new file mode 100644
index 000000000..9a33c228e
--- /dev/null
+++ b/.woodpecker/otp.yaml
@@ -0,0 +1,293 @@
+when:
+ - event: push
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
+ - event: tag
+ - event: manual
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ - event: manual
+ branch: stable
+
+matrix:
+ platform:
+ - linux/amd64
+ - linux/arm
+ - linux/arm64
+
+# This is needed for the when clauses below.
+# When the platform clause is fixed, this might not be needed anymore
+labels:
+ platform: ${platform}
+
+variables:
+ script_file_entrypoint: &script_file_entrypoint
+ - /bin/sh
+ - -c
+ - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
+ build_cmds: &build_cmds
+ - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip
+ - echo "import Config" > config/prod.secret.exs
+ - mix local.hex --force
+ - mix local.rebar --force
+ - mix deps.get --only prod
+ - mkdir release
+ - export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH}
+ - mix release --path release
+ build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716
+ build_image_arm: &build_image_arm docker.io/arm32v7/elixir:1.17.3
+ build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716
+ artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0
+ artifacts_uploader_settings: &artifacts_uploader_settings
+ user:
+ from_secret: pleroma-ci-user
+ password:
+ from_secret: pleroma-ci-password
+ owner: 'pleroma'
+ env: &env
+ MIX_ENV: prod
+ VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS
+ DEBIAN_FRONTEND: noninteractive
+
+steps:
+ otp-develop-amd64:
+ image: *build_image_amd64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ environment: *env
+ commands: &amd64_build
+ - <<: *build_cmds
+ - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release
+
+ otp-stable-amd64:
+ image: *build_image_amd64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
+ environment: *env
+ commands: *amd64_build
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ otp-stable-tag-amd64:
+ image: *build_image_amd64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
+ environment: *env
+ commands:
+ - <<: *build_cmds
+ - zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64.zip release
+
+ otp-develop-arm:
+ image: *build_image_arm
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ environment: *env
+ commands: &arm_build
+ - <<: *build_cmds
+ - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip release
+
+ otp-stable-arm:
+ image: *build_image_arm
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
+ environment: *env
+ commands: *arm_build
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ otp-stable-tag-arm:
+ image: *build_image_arm
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
+ environment: *env
+ commands:
+ - <<: *build_cmds
+ - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm.zip release
+
+ otp-develop-arm64:
+ image: *build_image_arm64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ environment: *env
+ commands: &arm64_build
+ - <<: *build_cmds
+ - zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release
+
+ otp-stable-arm64:
+ image: *build_image_arm64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
+ environment: *env
+ commands: *arm64_build
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ otp-stable-tag-arm64:
+ image: *build_image_arm64
+ entrypoint: *script_file_entrypoint
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
+ environment: *env
+ commands:
+ - <<: *build_cmds
+ - zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm64.zip release
+
+ upload-artifacts-amd64:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64
+ package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip
+ file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip
+ update: 'true'
+
+ upload-latest-amd64:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64
+ package_version: latest
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip
+ file_name: pleroma.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-artifacts-tag-amd64:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-amd64
+ package_version: stable-${CI_COMMIT_SHA:0:8}-amd64
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip
+ file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-latest-tag-amd64:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-amd64
+ package_version: latest
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip
+ file_name: pleroma.zip
+ update: 'true'
+
+ upload-artifacts-arm:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm
+ package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip
+ file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip
+ update: 'true'
+
+ upload-latest-arm:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm
+ package_version: latest
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip
+ file_name: pleroma.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-artifacts-tag-arm:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-arm
+ package_version: stable-${CI_COMMIT_SHA:0:8}-arm
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip
+ file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-latest-tag-arm:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-arm
+ package_version: latest
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip
+ file_name: pleroma.zip
+ update: 'true'
+
+ upload-artifacts-arm64:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64
+ package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip
+ file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip
+ update: 'true'
+
+ upload-latest-arm64:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64
+ package_version: latest
+ file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip
+ file_name: pleroma.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-artifacts-tag-arm64:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-arm64
+ package_version: stable-${CI_COMMIT_SHA:0:8}-arm64
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip
+ file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip
+ update: 'true'
+
+ # Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
+ upload-latest-tag-arm64:
+ image: *artifacts_uploader_image
+ when:
+ - evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
+ settings:
+ <<: *artifacts_uploader_settings
+ package_name: pleroma-otp-stable-arm64
+ package_version: latest
+ file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip
+ file_name: pleroma.zip
+ update: 'true'
diff --git a/.woodpecker.yml b/.woodpecker/unit-testing-elixir-1.15.yaml
similarity index 51%
rename from .woodpecker.yml
rename to .woodpecker/unit-testing-elixir-1.15.yaml
index f6b06dc61..a4a8fc266 100644
--- a/.woodpecker.yml
+++ b/.woodpecker/unit-testing-elixir-1.15.yaml
@@ -1,10 +1,26 @@
when:
- - event:
- - pull_request
+ - event: pull_request
+ path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
+ - event: push
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
+
+depends_on:
+ - lint
+
+labels:
+ platform: linux/amd64
+
+variables:
+ script_file_entrypoint: &script_file_entrypoint
+ - /bin/sh
+ - -c
+ - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
steps:
- test:
- image: elixir:1.19-alpine
+ unit-testing-elixir-1.15:
+ image: elixir:1.15-alpine
+ entrypoint: *script_file_entrypoint
environment:
MIX_ENV: test
DB_HOST: postgres
@@ -17,7 +33,7 @@ steps:
- su testuser -c "HOME=/home/testuser mix local.hex --force"
- su testuser -c "HOME=/home/testuser mix local.rebar --force"
- su testuser -c "HOME=/home/testuser mix deps.get"
- - su testuser -c "HOME=/home/testuser mix test"
+ - su testuser -c "HOME=/home/testuser mix pleroma.test_runner --preload-modules"
services:
postgres:
diff --git a/.woodpecker/unit-testing-elixir-1.18.yaml b/.woodpecker/unit-testing-elixir-1.18.yaml
new file mode 100644
index 000000000..9ad9eebc9
--- /dev/null
+++ b/.woodpecker/unit-testing-elixir-1.18.yaml
@@ -0,0 +1,44 @@
+when:
+ - event: pull_request
+ path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
+ - event: push
+ branch: ${CI_REPO_DEFAULT_BRANCH}
+ path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
+
+depends_on:
+ - lint
+
+labels:
+ platform: linux/amd64
+
+variables:
+ script_file_entrypoint: &script_file_entrypoint
+ - /bin/sh
+ - -c
+ - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
+
+steps:
+ unit-testing-elixir-1.18:
+ image: elixir:1.18-otp-27-alpine
+ entrypoint: *script_file_entrypoint
+ environment:
+ MIX_ENV: test
+ DB_HOST: postgres
+ DB_PORT: 5432
+ commands:
+ - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl
+ - adduser -D -h /home/testuser testuser
+ - mkdir -p /home/testuser/.mix /home/testuser/.hex
+ - chown -R testuser:testuser . /home/testuser
+ - su testuser -c "HOME=/home/testuser mix local.hex --force"
+ - su testuser -c "HOME=/home/testuser mix local.rebar --force"
+ - su testuser -c "HOME=/home/testuser mix deps.get"
+ - su testuser -c "HOME=/home/testuser mix pleroma.test_runner --preload-modules"
+
+services:
+ postgres:
+ image: postgres:13-alpine
+ environment:
+ POSTGRES_DB: pleroma_test
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
diff --git a/CHANGELOG.md b/CHANGELOG.md
index adc76c767..f03be3f29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,60 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+## 2.10.2
+
+### Security
+
+- ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed
+
+## 2.10.1
+
+### Changed
+
+- Move avatar_description and header_description fields to the account object
+- Update Bandit to 1.10.4
+- No-op code correctness improvements detected by Elixir 1.19 compiler
+- Downgrade Hackney to 1.20.1
+- Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney
+- Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation
+- Paginate follow requests
+- Moved Phoenix LiveDashboard to /pleroma/live_dashboard
+- Add mute/block expiry to the relationship object
+- Filter indexable activities before inserting indexing jobs into the queue.
+
+### Added
+
+- Allow assigning users to reports
+- Allow fine-grained announce visibilities
+- Add immutable tag on cache-control header for several endpoints that's serving the same exact things.
+- Add reasonable defaults for :database_config_whitelist
+- Support lists `exclusive` param
+- Add v1/instance/domain_blocks endpoint
+- Add /api/v2/instance profile fields limits info used by Mastodon
+- Added Oban Web dashboard located at /pleroma/oban
+- Add instructions on how to run a release in docker, to make it easier to run on older distros.
+
+### Fixed
+
+- Fix the daily email digest job which was not executing
+- Encode custom emoji URLs in EmojiReact activity tags.
+- Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled
+- Fix fetching Hubzilla Actors with alsoKnownAs as string
+- Fix /phoenix/live_dashboard redirect not working when user added a path segment
+- Fix 404 error codes for missing static files
+- Fix OAuth app registration to accept `redirect_uris` as an array of strings (RFC 7591), while keeping backwards compatibility with string input.
+- Correct old migrations for expiring activities and user access tokens.
+- Federate `votersCount` correctly
+- DB prune: Check if user follows hashtag with no objects before deletion
+- Stop the rate limiter from crashing when run with wrong settings.
+- Restore embeds route
+- ReverseProxy: Recursively follow redirects until redirect_limit is reached
+- Fix compilation with vips-8.18.0 with bumping to vix 0.36.0
+
+### Removed
+
+- Docs: Removed outdated, incorrect, unmaintained and inappropriate installation documentation (Arch, NetBSD, NixOS)
+
## 2.10
### Security
diff --git a/changelog.d/assign-users.add b/changelog.d/assign-users.add
deleted file mode 100644
index f50ad94c6..000000000
--- a/changelog.d/assign-users.add
+++ /dev/null
@@ -1 +0,0 @@
-Allow assigning users to reports
\ No newline at end of file
diff --git a/changelog.d/avatar-description-mastodon-api.change b/changelog.d/avatar-description-mastodon-api.change
deleted file mode 100644
index 6a454c01e..000000000
--- a/changelog.d/avatar-description-mastodon-api.change
+++ /dev/null
@@ -1 +0,0 @@
-Move avatar_description and header_description fields to the account object
diff --git a/changelog.d/bookmark-folders.ignore b/changelog.d/bookmark-folders.ignore
deleted file mode 100644
index 8705ac00b..000000000
--- a/changelog.d/bookmark-folders.ignore
+++ /dev/null
@@ -1 +0,0 @@
-Various bookmark folders-related improvements
diff --git a/changelog.d/boost-visibilities.add b/changelog.d/boost-visibilities.add
deleted file mode 100644
index 317d9840d..000000000
--- a/changelog.d/boost-visibilities.add
+++ /dev/null
@@ -1 +0,0 @@
-Allow fine-grained announce visibilities
diff --git a/changelog.d/ci-artifacts.skip b/changelog.d/ci-artifacts.skip
deleted file mode 100644
index e69de29bb..000000000
diff --git a/changelog.d/context-cleanup.skip b/changelog.d/context-cleanup.skip
new file mode 100644
index 000000000..ae609602d
--- /dev/null
+++ b/changelog.d/context-cleanup.skip
@@ -0,0 +1 @@
+litepub-0.1.jsonld cleanup
diff --git a/changelog.d/database-config-whitelist.add b/changelog.d/database-config-whitelist.add
deleted file mode 100644
index a78960c98..000000000
--- a/changelog.d/database-config-whitelist.add
+++ /dev/null
@@ -1 +0,0 @@
-Add reasonable defaults for :database_config_whitelist
\ No newline at end of file
diff --git a/changelog.d/elixir-1.19-cherrypicks.change b/changelog.d/elixir-1.19-cherrypicks.change
deleted file mode 100644
index 7e56be008..000000000
--- a/changelog.d/elixir-1.19-cherrypicks.change
+++ /dev/null
@@ -1 +0,0 @@
-No-op code correctness improvements detected by Elixir 1.19 compiler
diff --git a/changelog.d/emoji-reaction-url-escape.fix b/changelog.d/emoji-reaction-url-escape.fix
deleted file mode 100644
index c3a1c8823..000000000
--- a/changelog.d/emoji-reaction-url-escape.fix
+++ /dev/null
@@ -1 +0,0 @@
-Encode custom emoji URLs in EmojiReact activity tags.
diff --git a/changelog.d/exclusive-lists.add b/changelog.d/exclusive-lists.add
deleted file mode 100644
index bbd722f07..000000000
--- a/changelog.d/exclusive-lists.add
+++ /dev/null
@@ -1 +0,0 @@
-Support lists `exclusive` param
diff --git a/changelog.d/gopher-genserver-crash-on-boot.fix b/changelog.d/gopher-genserver-crash-on-boot.fix
deleted file mode 100644
index 3b51662be..000000000
--- a/changelog.d/gopher-genserver-crash-on-boot.fix
+++ /dev/null
@@ -1 +0,0 @@
-Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled
diff --git a/changelog.d/hackney-mediaproxy.change b/changelog.d/hackney-mediaproxy.change
deleted file mode 100644
index 10dfb0775..000000000
--- a/changelog.d/hackney-mediaproxy.change
+++ /dev/null
@@ -1 +0,0 @@
-Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney
diff --git a/changelog.d/hackney.change b/changelog.d/hackney.change
deleted file mode 100644
index 3158cfc77..000000000
--- a/changelog.d/hackney.change
+++ /dev/null
@@ -1 +0,0 @@
-Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation
diff --git a/changelog.d/http-signatures-0.1.3.fix b/changelog.d/http-signatures-0.1.3.fix
new file mode 100644
index 000000000..3be1b9e9f
--- /dev/null
+++ b/changelog.d/http-signatures-0.1.3.fix
@@ -0,0 +1 @@
+Fix compatibility with timestamped HTTP Signatures used by GoToSocial
diff --git a/changelog.d/hubzilla-alsoknownas.fix b/changelog.d/hubzilla-alsoknownas.fix
deleted file mode 100644
index 2a2969807..000000000
--- a/changelog.d/hubzilla-alsoknownas.fix
+++ /dev/null
@@ -1 +0,0 @@
-Fix fetching Hubzilla Actors with alsoKnownAs as string
diff --git a/changelog.d/iceshrimpnet-reports-fix.fix b/changelog.d/iceshrimpnet-reports-fix.fix
new file mode 100644
index 000000000..7487ae47e
--- /dev/null
+++ b/changelog.d/iceshrimpnet-reports-fix.fix
@@ -0,0 +1 @@
+Handle reports with just actor ap id as the object
diff --git a/changelog.d/inappropriate-docs.remove b/changelog.d/inappropriate-docs.remove
deleted file mode 100644
index 699c9186a..000000000
--- a/changelog.d/inappropriate-docs.remove
+++ /dev/null
@@ -1 +0,0 @@
-Docs: Removed outdated, incorrect, unmaintained and inappropriate installation documentation (Arch, NetBSD, NixOS)
diff --git a/changelog.d/instance-domain-blocks.add b/changelog.d/instance-domain-blocks.add
deleted file mode 100644
index 85f01c5c2..000000000
--- a/changelog.d/instance-domain-blocks.add
+++ /dev/null
@@ -1 +0,0 @@
-Add v1/instance/domain_blocks endpoint
diff --git a/changelog.d/instance-profile-fields.add b/changelog.d/instance-profile-fields.add
deleted file mode 100644
index 712bd68d9..000000000
--- a/changelog.d/instance-profile-fields.add
+++ /dev/null
@@ -1 +0,0 @@
-Add /api/v2/instance profile fields limits info used by Mastodon
diff --git a/changelog.d/majic-1.2.0.change b/changelog.d/majic-1.2.0.change
new file mode 100644
index 000000000..bc8d5eb3c
--- /dev/null
+++ b/changelog.d/majic-1.2.0.change
@@ -0,0 +1 @@
+Update majic to 1.2.0.
diff --git a/changelog.d/map-side-effects.skip b/changelog.d/map-side-effects.skip
deleted file mode 100644
index e69de29bb..000000000
diff --git a/changelog.d/mfm-backend.add b/changelog.d/mfm-backend.add
new file mode 100644
index 000000000..f815c6828
--- /dev/null
+++ b/changelog.d/mfm-backend.add
@@ -0,0 +1 @@
+Add backend support for Misskey Markdown (MFM) posts
diff --git a/changelog.d/mix-exs-fix.skip b/changelog.d/mix-exs-fix.skip
deleted file mode 100644
index e69de29bb..000000000
diff --git a/changelog.d/mix-exs-update.skip b/changelog.d/mix-exs-update.skip
deleted file mode 100644
index e69de29bb..000000000
diff --git a/changelog.d/oauth-registration-redirect_uris.fix b/changelog.d/oauth-registration-redirect_uris.fix
deleted file mode 100644
index 76ace55df..000000000
--- a/changelog.d/oauth-registration-redirect_uris.fix
+++ /dev/null
@@ -1 +0,0 @@
-Fix OAuth app registration to accept `redirect_uris` as an array of strings (RFC 7591), while keeping backwards compatibility with string input.
diff --git a/changelog.d/oban-plugins-lazarus-hex.change b/changelog.d/oban-plugins-lazarus-hex.change
new file mode 100644
index 000000000..9259df0f5
--- /dev/null
+++ b/changelog.d/oban-plugins-lazarus-hex.change
@@ -0,0 +1 @@
+Use the Hex package for oban_plugins_lazarus.
diff --git a/changelog.d/oban-web.add b/changelog.d/oban-web.add
deleted file mode 100644
index c59e2ebca..000000000
--- a/changelog.d/oban-web.add
+++ /dev/null
@@ -1 +0,0 @@
-Added Oban Web dashboard located at /pleroma/oban
diff --git a/changelog.d/old-migrations.fix b/changelog.d/old-migrations.fix
deleted file mode 100644
index 49566c896..000000000
--- a/changelog.d/old-migrations.fix
+++ /dev/null
@@ -1 +0,0 @@
-Correct old migrations for expiring activities and user access tokens.
diff --git a/changelog.d/paginate-follow-requests.change b/changelog.d/paginate-follow-requests.change
deleted file mode 100644
index 1a88995b7..000000000
--- a/changelog.d/paginate-follow-requests.change
+++ /dev/null
@@ -1 +0,0 @@
-Paginate follow requests
diff --git a/changelog.d/phoenix-livedashboard-move.change b/changelog.d/phoenix-livedashboard-move.change
deleted file mode 100644
index 116b1523a..000000000
--- a/changelog.d/phoenix-livedashboard-move.change
+++ /dev/null
@@ -1 +0,0 @@
-Moved Phoenix LiveDashboard to /pleroma/live_dashboard
diff --git a/changelog.d/pleroma-fe-link.fix b/changelog.d/pleroma-fe-link.fix
new file mode 100644
index 000000000..de93f86dd
--- /dev/null
+++ b/changelog.d/pleroma-fe-link.fix
@@ -0,0 +1 @@
+Updated Pleroma-FE build URL after Forgejo migration
diff --git a/changelog.d/plug-test-typo.skip b/changelog.d/plug-test-typo.skip
deleted file mode 100644
index e69de29bb..000000000
diff --git a/changelog.d/poll-voters-count-inflation.fix b/changelog.d/poll-voters-count-inflation.fix
new file mode 100644
index 000000000..7eae41f13
--- /dev/null
+++ b/changelog.d/poll-voters-count-inflation.fix
@@ -0,0 +1 @@
+Fix votersCount inflation when same voter picks multiple options
diff --git a/changelog.d/prune-hashtag-follow-3376.fix b/changelog.d/prune-hashtag-follow-3376.fix
deleted file mode 100644
index cdb4e9a79..000000000
--- a/changelog.d/prune-hashtag-follow-3376.fix
+++ /dev/null
@@ -1 +0,0 @@
-DB prune: Check if user follows hashtag with no objects before deletion
diff --git a/changelog.d/reduce-flaky-tests.skip b/changelog.d/reduce-flaky-tests.skip
deleted file mode 100644
index 0375762c0..000000000
--- a/changelog.d/reduce-flaky-tests.skip
+++ /dev/null
@@ -1 +0,0 @@
-Reduce the number of flaky tests by making them sync if they affect the global state, and silence noisy test output.
diff --git a/changelog.d/reject-third-party-reports.fix b/changelog.d/reject-third-party-reports.fix
new file mode 100644
index 000000000..7d4e87b94
--- /dev/null
+++ b/changelog.d/reject-third-party-reports.fix
@@ -0,0 +1 @@
+Reject incoming reports when both the reporter and reported account are remote
diff --git a/changelog.d/relationship-expires-at.change b/changelog.d/relationship-expires-at.change
deleted file mode 100644
index 286dba197..000000000
--- a/changelog.d/relationship-expires-at.change
+++ /dev/null
@@ -1 +0,0 @@
-Add mute/block expiry to the relationship object
diff --git a/changelog.d/release-to-docker.add b/changelog.d/release-to-docker.add
deleted file mode 100644
index 5fbf611a5..000000000
--- a/changelog.d/release-to-docker.add
+++ /dev/null
@@ -1 +0,0 @@
-Add instructions on how to run a release in docker, to make it easier to run on older distros.
diff --git a/changelog.d/twitter-api.skip b/changelog.d/twitter-api.skip
deleted file mode 100644
index e69de29bb..000000000
diff --git a/changelog.d/update-comment.ignore b/changelog.d/update-comment.ignore
deleted file mode 100644
index 733e813b3..000000000
--- a/changelog.d/update-comment.ignore
+++ /dev/null
@@ -1 +0,0 @@
-Update comment for prepare_object, rename prepare_outgoing
diff --git a/changelog.d/user-search-sorting.change b/changelog.d/user-search-sorting.change
new file mode 100644
index 000000000..def27e955
--- /dev/null
+++ b/changelog.d/user-search-sorting.change
@@ -0,0 +1 @@
+Improve user search / autocompletion ordering.
diff --git a/changelog.d/user-view.ignore b/changelog.d/user-view.ignore
deleted file mode 100644
index 37e9a7e09..000000000
--- a/changelog.d/user-view.ignore
+++ /dev/null
@@ -1 +0,0 @@
-Avoid code duplication in UserView
diff --git a/changelog.d/vix-0.36.0.fix b/changelog.d/vix-0.36.0.fix
deleted file mode 100644
index 43a8dd8f8..000000000
--- a/changelog.d/vix-0.36.0.fix
+++ /dev/null
@@ -1 +0,0 @@
-Fix compilation with vips-8.18.0 with bumping to vix 0.36.0
diff --git a/config/config.exs b/config/config.exs
index 069c0c7c0..5420a8aa4 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -202,7 +202,8 @@ config :pleroma, :instance,
"text/plain",
"text/html",
"text/markdown",
- "text/bbcode"
+ "text/bbcode",
+ "text/x.misskeymarkdown"
],
autofollowed_nicknames: [],
autofollowing_nicknames: [],
@@ -774,7 +775,7 @@ config :pleroma, :frontends,
"name" => "pleroma-fe",
"git" => "https://git.pleroma.social/pleroma/pleroma-fe",
"build_url" =>
- "https://git.pleroma.social/pleroma/pleroma-fe/-/jobs/artifacts/${ref}/download?job=build",
+ "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-fe-builds/${ref}/latest.zip",
"ref" => "develop"
},
"fedi-fe" => %{
diff --git a/config/description.exs b/config/description.exs
index c388d17c3..6e4348907 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -815,7 +815,8 @@ config :pleroma, :config_description, [
"text/plain",
"text/html",
"text/markdown",
- "text/bbcode"
+ "text/bbcode",
+ "text/x.misskeymarkdown"
]
},
%{
@@ -1394,7 +1395,13 @@ config :pleroma, :config_description, [
label: "Post Content Type",
type: {:dropdown, :atom},
description: "Default post formatting option",
- suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"]
+ suggestions: [
+ "text/plain",
+ "text/html",
+ "text/markdown",
+ "text/bbcode",
+ "text/x.misskeymarkdown"
+ ]
},
%{
key: :redirectRootNoLogin,
diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex
index edce9e871..facc38815 100644
--- a/lib/mix/tasks/pleroma/search/meilisearch.ex
+++ b/lib/mix/tasks/pleroma/search/meilisearch.ex
@@ -72,7 +72,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
query,
timeout: :infinity
)
- |> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1)
+ |> Stream.map(&Pleroma.Search.object_to_search_data/1)
|> Stream.filter(fn o -> not is_nil(o) end)
|> Stream.chunk_every(chunk_size)
|> Stream.transform(0, fn objects, acc ->
diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex
index e9990fa35..2c3df773b 100644
--- a/lib/pleroma/config_db.ex
+++ b/lib/pleroma/config_db.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.ConfigDB do
import Pleroma.Web.Gettext
alias __MODULE__
+ alias Pleroma.EctoType.Config.RateLimit
alias Pleroma.Repo
@type t :: %__MODULE__{}
@@ -60,8 +61,59 @@ defmodule Pleroma.ConfigDB do
|> cast(params, [:key, :group, :value])
|> validate_required([:key, :group, :value])
|> unique_constraint(:key, name: :config_group_key_index)
+ |> validate_rate_limit()
end
+ defp validate_rate_limit(changeset) do
+ group = get_field(changeset, :group)
+ key = get_field(changeset, :key)
+
+ if group == :pleroma and key == :rate_limit do
+ value = get_field(changeset, :value)
+
+ case normalize_rate_limit(value) do
+ {:ok, normalized_value} ->
+ put_change(changeset, :value, normalized_value)
+
+ {:error, {limiter_name, reason}} ->
+ add_error(
+ changeset,
+ :value,
+ "invalid :rate_limit value for #{inspect(limiter_name)}: #{reason}"
+ )
+ end
+ else
+ changeset
+ end
+ end
+
+ defp normalize_rate_limit(nil), do: {:ok, nil}
+
+ defp normalize_rate_limit(%{} = value), do: normalize_rate_limit(Map.to_list(value))
+
+ defp normalize_rate_limit(value) when is_list(value) do
+ if Keyword.keyword?(value) do
+ value
+ |> Enum.reduce_while({:ok, []}, fn {limiter_name, limiter_value}, {:ok, acc} ->
+ case RateLimit.cast_with_error(limiter_value) do
+ {:ok, normalized_limiter_value} ->
+ {:cont, {:ok, [{limiter_name, normalized_limiter_value} | acc]}}
+
+ {:error, reason} ->
+ {:halt, {:error, {limiter_name, reason}}}
+ end
+ end)
+ |> case do
+ {:ok, acc} -> {:ok, Enum.reverse(acc)}
+ {:error, _} = error -> error
+ end
+ else
+ {:error, {:rate_limit, "must be a keyword list"}}
+ end
+ end
+
+ defp normalize_rate_limit(_), do: {:error, {:rate_limit, "must be a keyword list"}}
+
defp create(params) do
%ConfigDB{}
|> changeset(params)
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index f78b84099..178dd6094 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -29,7 +29,7 @@ defmodule Pleroma.Constants do
const(static_only_files,
do:
- ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
+ ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js schemas doc embed.js embed.css)
)
const(status_updatable_fields,
diff --git a/lib/pleroma/ecto_type/config/rate_limit.ex b/lib/pleroma/ecto_type/config/rate_limit.ex
new file mode 100644
index 000000000..0518ffd7e
--- /dev/null
+++ b/lib/pleroma/ecto_type/config/rate_limit.ex
@@ -0,0 +1,71 @@
+# Pleroma: A lightweight social networking server
+# Copyright © Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.Config.RateLimit do
+ @moduledoc false
+
+ use Ecto.Type
+
+ @type t ::
+ nil
+ | {non_neg_integer(), non_neg_integer()}
+ | [{non_neg_integer(), non_neg_integer()}]
+
+ @impl true
+ def type, do: :term
+
+ @impl true
+ def cast(value) do
+ case cast_with_error(value) do
+ {:ok, normalized} -> {:ok, normalized}
+ {:error, _reason} -> :error
+ end
+ end
+
+ @impl true
+ def load(value), do: cast(value)
+
+ @impl true
+ def dump(value), do: cast(value)
+
+ @spec cast_with_error(term()) :: {:ok, t()} | {:error, String.t()}
+ def cast_with_error(nil), do: {:ok, nil}
+
+ def cast_with_error({scale, limit}) do
+ with {:ok, scale} <- parse_integer(scale, "scale"),
+ {:ok, limit} <- parse_integer(limit, "limit"),
+ true <- scale >= 1 and limit >= 1 do
+ {:ok, {scale, limit}}
+ else
+ false -> {:error, "scale and limit must be >= 1"}
+ {:error, reason} -> {:error, reason}
+ end
+ end
+
+ def cast_with_error([{_, _} = unauth, {_, _} = auth]) do
+ with {:ok, unauth} <- cast_with_error(unauth),
+ {:ok, auth} <- cast_with_error(auth) do
+ {:ok, [unauth, auth]}
+ else
+ {:error, reason} -> {:error, reason}
+ end
+ end
+
+ def cast_with_error(_),
+ do:
+ {:error, "must be a {scale, limit} tuple, a [{scale, limit}, {scale, limit}] list, or nil"}
+
+ defp parse_integer(value, _label) when is_integer(value), do: {:ok, value}
+
+ defp parse_integer(value, label) when is_binary(value) do
+ value = String.trim(value)
+
+ case Integer.parse(value) do
+ {number, ""} -> {:ok, number}
+ _ -> {:error, "#{label} must be an integer"}
+ end
+ end
+
+ defp parse_integer(_value, label), do: {:error, "#{label} must be an integer"}
+end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 11d5af2fb..4bf2f6b95 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -127,6 +127,13 @@ defmodule Pleroma.Formatter do
Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false})
end
+ def markdown_to_html(text, opts) do
+ Earmark.as_html!(
+ text,
+ %Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts)
+ )
+ end
+
def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end
@@ -135,6 +142,10 @@ defmodule Pleroma.Formatter do
HTML.filter_tags(text)
end
+ def html_escape(text, "text/x.misskeymarkdown") do
+ HTML.filter_tags(text)
+ end
+
def html_escape(text, "text/plain") do
Regex.split(@link_regex, text, include_captures: true)
|> Enum.map_every(2, fn chunk ->
diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex
index e651d7d9d..e37a3fefe 100644
--- a/lib/pleroma/frontend.ex
+++ b/lib/pleroma/frontend.ex
@@ -75,8 +75,8 @@ defmodule Pleroma.Frontend do
end
defp download_build(frontend_info, dest) do
- Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}")
url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"])
+ Logger.info("Downloading pre-built bundle for #{frontend_info["name"]} from #{url}")
with {:ok, %{status: 200, body: zip_body}} <-
Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index d0cb16b79..5e9314446 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -372,12 +372,28 @@ defmodule Pleroma.Object do
option
end)
- voters = [actor | object.data["voters"] || []] |> Enum.uniq()
+ existing_voters = object.data["voters"] || []
+ voters = [actor | existing_voters] |> Enum.uniq()
+ new_voter? = actor not in existing_voters
+ existing_voters_count = object.data["votersCount"]
+
+ voters_count =
+ cond do
+ is_integer(existing_voters_count) and new_voter? ->
+ existing_voters_count + 1
+
+ is_integer(existing_voters_count) ->
+ existing_voters_count
+
+ true ->
+ length(voters)
+ end
data =
object.data
|> Map.put(key, options)
|> Map.put("voters", voters)
+ |> Map.put("votersCount", voters_count)
object
|> Object.change(%{data: data})
diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex
index af2d35c8f..49400940f 100644
--- a/lib/pleroma/release_tasks.ex
+++ b/lib/pleroma/release_tasks.ex
@@ -5,7 +5,10 @@
defmodule Pleroma.ReleaseTasks do
@repo Pleroma.Repo
- def run(args) do
+ # TODO: Kept for some backwards compatibility with buggy pleroma_ctl,
+ # if a mismatch between pleroma_ctl and Pleroma accidentaly happens.
+ # Remove in the future.
+ def run(args) when is_binary(args) do
[task | args] = String.split(args)
case task do
@@ -16,6 +19,20 @@ defmodule Pleroma.ReleaseTasks do
end
end
+ # HACK: Script arguments need to be received as a list, otherwise (quoted) arguments with
+ # whitespace will be broken. Previously the broken string form above was used,
+ # escaping in the shell does not help.
+ def run(args) when is_list(args) do
+ [task | args] = args
+
+ case task do
+ "migrate" -> migrate(args)
+ "create" -> create()
+ "rollback" -> rollback(args)
+ task -> mix_task(task, args)
+ end
+ end
+
def find_module(task) do
module_name =
task
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
index bb55a4984..c7ee47c6e 100644
--- a/lib/pleroma/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.ReverseProxy do
@keep_resp_headers @resp_cache_headers ++
~w(content-length content-type content-disposition content-encoding) ++
~w(content-range accept-ranges vary)
- @default_cache_control_header "public, max-age=1209600"
+ @default_cache_control_header "public, max-age=1209600, immutable"
@valid_resp_codes [200, 206, 304]
@max_read_duration :timer.seconds(30)
@max_body_length :infinity
diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex
index 7e1fca80d..4c81a6225 100644
--- a/lib/pleroma/reverse_proxy/client/hackney.ex
+++ b/lib/pleroma/reverse_proxy/client/hackney.ex
@@ -4,6 +4,9 @@
defmodule Pleroma.ReverseProxy.Client.Hackney do
@behaviour Pleroma.ReverseProxy.Client
+ @redirect_limit 5
+
+ require Logger
# In-app redirect handler to avoid Hackney redirect bugs:
# - https://github.com/benoitc/hackney/issues/527 (relative/protocol-less redirects can crash Hackney)
@@ -31,26 +34,15 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
if opts[:follow_redirect] != false do
{_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end)
+ env = %{method: method, headers: headers, body: body, req_opts: req_opts}
res = :hackney.request(method, url, headers, body, req_opts)
case res do
{:ok, code, resp_headers, _client} when code in @redirect_statuses ->
- :hackney.request(
- method,
- absolute_redirect_url(url, resp_headers),
- headers,
- body,
- req_opts
- )
+ redirect(url, resp_headers, env, @redirect_limit)
{:ok, code, resp_headers} when code in @redirect_statuses ->
- :hackney.request(
- method,
- absolute_redirect_url(url, resp_headers),
- headers,
- body,
- req_opts
- )
+ redirect(url, resp_headers, env, @redirect_limit)
_ ->
res
@@ -71,4 +63,32 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
@impl true
def close(ref), do: :hackney.close(ref)
+
+ defp redirect(url, resp_headers, env, limit) when limit == 0 do
+ new_url = absolute_redirect_url(url, resp_headers)
+
+ Logger.debug(
+ "#{__MODULE__}: Handling redirect #{url} -> #{new_url}; redirect limit was reached - returning response after final redirect"
+ )
+
+ :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts)
+ end
+
+ defp redirect(url, resp_headers, env, limit) do
+ new_url = absolute_redirect_url(url, resp_headers)
+ Logger.debug("#{__MODULE__}: handling redirect #{url} -> #{new_url}; limit = #{limit}")
+
+ res = :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts)
+
+ case res do
+ {:ok, code, new_resp_headers, _client} when code in @redirect_statuses ->
+ redirect(new_url, new_resp_headers, env, limit - 1)
+
+ {:ok, code, new_resp_headers} when code in @redirect_statuses ->
+ redirect(new_url, new_resp_headers, env, limit - 1)
+
+ _ ->
+ res
+ end
+ end
end
diff --git a/lib/pleroma/search.ex b/lib/pleroma/search.ex
index 30b3ba958..9cd2768c4 100644
--- a/lib/pleroma/search.ex
+++ b/lib/pleroma/search.ex
@@ -1,11 +1,28 @@
defmodule Pleroma.Search do
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Workers.SearchIndexingWorker
- def add_to_index(%Pleroma.Activity{id: activity_id}) do
- SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => activity_id})
- |> Oban.insert()
+ @spec add_to_index(Activity.t()) :: {:ok, Oban.Job.t() | :noop} | {:error, Oban.Job.changeset()}
+ def add_to_index(%Activity{id: activity_id, object: %Object{} = object} = activity) do
+ with {_, true} <- {:indexable, indexable?(activity)},
+ {_, "public"} <- {:visibility, Visibility.get_visibility(object)} do
+ SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => activity_id})
+ |> Oban.insert()
+ else
+ _ -> {:ok, :noop}
+ end
end
+ def add_to_index(%Activity{id: activity_id}) do
+ case Activity.get_by_id_with_object(activity_id) do
+ %Activity{} = preloaded -> add_to_index(preloaded)
+ _ -> {:ok, :noop}
+ end
+ end
+
+ @spec remove_from_index(Object.t()) :: {:ok, Oban.Job.t()} | {:error, Oban.Job.changeset()}
def remove_from_index(%Pleroma.Object{id: object_id}) do
SearchIndexingWorker.new(%{"op" => "remove_from_index", "object" => object_id})
|> Oban.insert()
@@ -20,4 +37,44 @@ defmodule Pleroma.Search do
search_module = Pleroma.Config.get([Pleroma.Search, :module])
search_module.healthcheck_endpoints()
end
+
+ def object_to_search_data(%Object{} = object) do
+ data = object.data
+
+ content_str =
+ case data["content"] do
+ [nil | rest] -> to_string(rest)
+ str -> str
+ end
+
+ content =
+ with {:ok, scrubbed} <-
+ FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing),
+ trimmed <- String.trim(scrubbed) do
+ trimmed
+ end
+
+ # Make sure we have a non-empty string
+ if content != "" do
+ {:ok, published, _} = DateTime.from_iso8601(data["published"])
+
+ %{
+ id: object.id,
+ content: content,
+ ap: data["id"],
+ published: published |> DateTime.to_unix()
+ }
+ end
+ end
+
+ defp indexable?(%Activity{
+ data: %{"type" => "Create"},
+ object: %Object{
+ data: %{"content" => content, "published" => published, "type" => "Note"}
+ }
+ })
+ when not is_nil(content) and content not in ["", "."] and not is_nil(published),
+ do: true
+
+ defp indexable?(_), do: false
end
diff --git a/lib/pleroma/search/meilisearch.ex b/lib/pleroma/search/meilisearch.ex
index cafae8099..dc10076e1 100644
--- a/lib/pleroma/search/meilisearch.ex
+++ b/lib/pleroma/search/meilisearch.ex
@@ -4,6 +4,8 @@ defmodule Pleroma.Search.Meilisearch do
alias Pleroma.Activity
alias Pleroma.Config.Getting, as: Config
+ alias Pleroma.Object
+ alias Pleroma.Search
import Pleroma.Search.DatabaseSearch
import Ecto.Query
@@ -118,66 +120,24 @@ defmodule Pleroma.Search.Meilisearch do
end
end
- def object_to_search_data(object) do
- # Only index public or unlisted Notes
- if not is_nil(object) and object.data["type"] == "Note" and
- not is_nil(object.data["content"]) and
- not is_nil(object.data["published"]) and
- (Pleroma.Constants.as_public() in object.data["to"] or
- Pleroma.Constants.as_public() in object.data["cc"]) and
- object.data["content"] not in ["", "."] do
- data = object.data
-
- content_str =
- case data["content"] do
- [nil | rest] -> to_string(rest)
- str -> str
- end
-
- content =
- with {:ok, scrubbed} <-
- FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing),
- trimmed <- String.trim(scrubbed) do
- trimmed
- end
-
- # Make sure we have a non-empty string
- if content != "" do
- {:ok, published, _} = DateTime.from_iso8601(data["published"])
-
- %{
- id: object.id,
- content: content,
- ap: data["id"],
- published: published |> DateTime.to_unix()
- }
- end
- end
- end
-
@impl true
- def add_to_index(activity) do
- maybe_search_data = object_to_search_data(activity.object)
+ def add_to_index(%Activity{object: %Object{} = object} = activity) do
+ search_data = Search.object_to_search_data(object)
- if activity.data["type"] == "Create" and maybe_search_data do
- result =
- meili_put(
- "/indexes/objects/documents",
- [maybe_search_data]
- )
+ result =
+ meili_put(
+ "/indexes/objects/documents",
+ [search_data]
+ )
- with {:ok, %{"status" => "enqueued"}} <- result do
- # Added successfully
- :ok
- else
- _ ->
- # There was an error, report it
- Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
- {:error, result}
- end
- else
- # The post isn't something we can search, that's ok
+ with {:ok, %{"status" => "enqueued"}} <- result do
+ # Added successfully
:ok
+ else
+ _ ->
+ # There was an error, report it
+ Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
+ {:error, result}
end
end
diff --git a/lib/pleroma/search/qdrant_search.ex b/lib/pleroma/search/qdrant_search.ex
index 5142a273f..4d57cfa88 100644
--- a/lib/pleroma/search/qdrant_search.ex
+++ b/lib/pleroma/search/qdrant_search.ex
@@ -4,11 +4,12 @@ defmodule Pleroma.Search.QdrantSearch do
alias Pleroma.Activity
alias Pleroma.Config.Getting, as: Config
+ alias Pleroma.Object
+ alias Pleroma.Search
alias __MODULE__.OpenAIClient
alias __MODULE__.QdrantClient
- import Pleroma.Search.Meilisearch, only: [object_to_search_data: 1]
import Pleroma.Search.DatabaseSearch, only: [maybe_fetch: 3]
@impl true
@@ -82,23 +83,18 @@ defmodule Pleroma.Search.QdrantSearch do
end
@impl true
- def add_to_index(activity) do
- # This will only index public or unlisted notes
- maybe_search_data = object_to_search_data(activity.object)
+ def add_to_index(%Activity{object: %Object{} = object} = activity) do
+ search_data = Search.object_to_search_data(object)
- if activity.data["type"] == "Create" and maybe_search_data do
- with {:ok, embedding} <- get_embedding(maybe_search_data.content),
- {:ok, %{status: 200}} <-
- QdrantClient.put(
- "/collections/posts/points",
- build_index_payload(activity, embedding)
- ) do
- :ok
- else
- e -> {:error, e}
- end
- else
+ with {:ok, embedding} <- get_embedding(search_data.content),
+ {:ok, %{status: 200}} <-
+ QdrantClient.put(
+ "/collections/posts/points",
+ build_index_payload(activity, embedding)
+ ) do
:ok
+ else
+ e -> {:error, e}
end
end
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
index fca61799b..382cf9db3 100644
--- a/lib/pleroma/signature.ex
+++ b/lib/pleroma/signature.ex
@@ -104,7 +104,7 @@ defmodule Pleroma.Signature do
|> put_req_header("(request-target)", request_target)
|> put_req_header("@request-target", request_target)
- @http_signatures_impl.validate_conn(conn)
+ @http_signatures_impl.validate_conn(conn) == true
end
@spec validate_signature(Plug.Conn.t()) :: boolean()
diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex
index 851745714..abc45f62a 100644
--- a/lib/pleroma/user/search.ex
+++ b/lib/pleroma/user/search.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.User.Search do
alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType
+ alias Pleroma.Instances.Instance
alias Pleroma.Pagination
alias Pleroma.User
@@ -88,12 +89,13 @@ defmodule Pleroma.User.Search do
|> filter_invisible_users()
|> filter_internal_users()
|> filter_blocked_domains(for_user)
+ |> filter_unreachable_users()
|> fts_search(query_string)
|> select_top_users(top_user_ids)
|> trigram_rank(query_string)
|> boost_search_rank(for_user, top_user_ids)
|> subquery()
- |> order_by(desc: :search_rank)
+ |> order_by_search_rank(for_user)
|> maybe_restrict_local(for_user)
|> maybe_restrict_accepting_chat_messages(capabilities)
|> filter_deactivated_users()
@@ -196,6 +198,14 @@ defmodule Pleroma.User.Search do
defp filter_blocked_domains(query, _), do: query
+ defp filter_unreachable_users(query) do
+ from(u in query,
+ left_join: i in Instance,
+ on: i.host == fragment("substring(? from '.*://([^/]*)')", u.ap_id),
+ where: is_nil(i.unreachable_since)
+ )
+ end
+
defp maybe_resolve(true, user, query) do
case {limit(), user} do
{:all, _} -> :noop
@@ -236,6 +246,16 @@ defmodule Pleroma.User.Search do
from(u in subquery(query),
select_merge: %{
+ search_type:
+ fragment(
+ """
+ CASE WHEN (?) THEN 2
+ WHEN (?) THEN 1
+ ELSE 0 END
+ """,
+ u.id in ^top_user_ids,
+ u.id in ^friends_ids or u.id in ^followers_ids
+ ),
search_rank:
fragment(
"""
@@ -261,6 +281,14 @@ defmodule Pleroma.User.Search do
defp boost_search_rank(query, _for_user, top_user_ids) do
from(u in subquery(query),
select_merge: %{
+ search_type:
+ fragment(
+ """
+ CASE WHEN (?) THEN 2
+ ELSE 0 END
+ """,
+ u.id in ^top_user_ids
+ ),
search_rank:
fragment(
"""
@@ -273,4 +301,22 @@ defmodule Pleroma.User.Search do
}
)
end
+
+ defp order_by_search_rank(query, %User{}) do
+ order_by(
+ query,
+ [u],
+ desc: u.search_type,
+ desc_nulls_last:
+ fragment(
+ "CASE WHEN ? = 1 THEN COALESCE(?, ?) ELSE NULL END",
+ u.search_type,
+ u.last_status_at,
+ u.last_active_at
+ ),
+ desc: u.search_rank
+ )
+ end
+
+ defp order_by_search_rank(query, _), do: order_by(query, desc: :search_rank)
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 071d634db..0b513ee16 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1677,44 +1677,80 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
show_birthday = !!birthday
- # if WebFinger request was already done, we probably have acct, otherwise
- # we request WebFinger here
- nickname = additional[:nickname_from_acct] || generate_nickname(data)
+ with {:ok, nickname} <- nickname_from_actor(data, additional) do
+ {:ok,
+ %{
+ ap_id: data["id"],
+ uri: get_actor_url(data["url"]),
+ banner: normalize_image(data["image"]),
+ fields: fields,
+ emoji: emojis,
+ is_locked: is_locked,
+ is_discoverable: is_discoverable,
+ invisible: invisible,
+ avatar: normalize_image(data["icon"]),
+ name: data["name"],
+ follower_address: data["followers"],
+ following_address: data["following"],
+ featured_address: featured_address,
+ bio: data["summary"] || "",
+ actor_type: actor_type,
+ also_known_as: normalize_also_known_as(data["alsoKnownAs"]),
+ public_key: public_key,
+ inbox: data["inbox"],
+ shared_inbox: shared_inbox,
+ accepts_chat_messages: accepts_chat_messages,
+ birthday: birthday,
+ show_birthday: show_birthday,
+ pinned_objects: pinned_objects,
+ nickname: nickname
+ }}
+ end
+ end
- %{
- ap_id: data["id"],
- uri: get_actor_url(data["url"]),
- banner: normalize_image(data["image"]),
- fields: fields,
- emoji: emojis,
- is_locked: is_locked,
- is_discoverable: is_discoverable,
- invisible: invisible,
- avatar: normalize_image(data["icon"]),
- name: data["name"],
- follower_address: data["followers"],
- following_address: data["following"],
- featured_address: featured_address,
- bio: data["summary"] || "",
- actor_type: actor_type,
- also_known_as: normalize_also_known_as(data["alsoKnownAs"]),
- public_key: public_key,
- inbox: data["inbox"],
- shared_inbox: shared_inbox,
- accepts_chat_messages: accepts_chat_messages,
- birthday: birthday,
- show_birthday: show_birthday,
- pinned_objects: pinned_objects,
- nickname: nickname
- }
+ defp nickname_from_actor(data, additional) do
+ generated = generated_nickname(data)
+
+ case additional[:nickname_from_acct] do
+ ^generated when is_binary(generated) ->
+ {:ok, generated}
+
+ acct when is_binary(acct) ->
+ with ^acct <- webfinger_nickname(data) do
+ {:ok, acct}
+ else
+ _ -> {:error, {:webfinger_actor_mismatch, acct, data["id"]}}
+ end
+
+ _ ->
+ {:ok, generate_nickname(data)}
+ end
+ end
+
+ defp generated_nickname(%{"preferredUsername" => username, "id" => ap_id})
+ when is_binary(username) and is_binary(ap_id) do
+ case URI.parse(ap_id) do
+ %URI{host: host} when is_binary(host) -> "#{username}@#{host}"
+ _ -> nil
+ end
+ end
+
+ defp generated_nickname(_), do: nil
+
+ defp webfinger_nickname(data) do
+ with generated when is_binary(generated) <- generated_nickname(data),
+ {:ok, %{"subject" => "acct:" <> acct, "ap_id" => ap_id}} <- WebFinger.finger(generated),
+ true <- ap_id == data["id"] do
+ acct
+ end
end
defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do
- generated = "#{username}@#{URI.parse(data["id"]).host}"
+ generated = generated_nickname(data)
if Config.get([WebFinger, :update_nickname_on_user_fetch]) do
- case WebFinger.finger(generated) do
- {:ok, %{"subject" => "acct:" <> acct}} -> acct
+ case webfinger_nickname(data) do
+ acct when is_binary(acct) -> acct
_ -> generated
end
else
@@ -1794,9 +1830,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp collection_private(_data), do: {:ok, true}
def user_data_from_user_object(data, additional \\ []) do
- with {:ok, data} <- MRF.filter(data) do
- {:ok, object_to_user_data(data, additional)}
+ with {:ok, data} <- MRF.filter(data),
+ {:ok, data} <- object_to_user_data(data, additional) do
+ {:ok, data}
else
+ {:error, _} = e -> e
e -> {:error, e}
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 4f1613a07..2bfff6968 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -348,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
- Federator.incoming_ap_doc(%{
+ Federator.incoming_failed_signature_ap_doc(%{
method: conn.method,
req_headers: conn.req_headers,
request_path: conn.request_path,
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index c0626ce4d..844ad2c8f 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -6,9 +6,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.HTML
+ alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.CommonAPI.Utils
import Ecto.Changeset
@@ -26,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
end
field(:replies, {:array, ObjectValidators.ObjectID}, default: [])
+ field(:source, :map)
end
def cast_and_apply(data) do
@@ -80,6 +84,113 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
def fix_attachments(data), do: data
+ defp remote_mention_resolver(
+ %{"id" => ap_id, "tag" => tags},
+ "@" <> nickname = mention,
+ buffer,
+ opts,
+ acc
+ )
+ when is_binary(ap_id) and is_list(tags) do
+ initial_host =
+ ap_id
+ |> URI.parse()
+ |> Map.get(:host)
+
+ with mention_tag when not is_nil(mention_tag) <-
+ Enum.find(tags, &mention_tag?(&1, mention, initial_host)),
+ href when is_binary(href) <- mention_tag["href"],
+ %User{} = user <- User.get_cached_by_ap_id(href) do
+ link = Pleroma.Formatter.mention_from_user(user, opts)
+ {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
+ else
+ _ -> {buffer, acc}
+ end
+ end
+
+ defp remote_mention_resolver(_object, _mention, buffer, _opts, acc), do: {buffer, acc}
+
+ defp mention_tag?(%{"type" => "Mention", "name" => name}, mention, initial_host)
+ when is_binary(name) do
+ name == mention || mention == "#{name}@#{initial_host}"
+ end
+
+ defp mention_tag?(_tag, _mention, _initial_host), do: false
+
+ defp scrub_content(%{"content" => content} = object) when is_binary(content) do
+ Map.put(object, "content", HTML.filter_tags(content))
+ end
+
+ defp scrub_content(object), do: object
+
+ defp mfm_parse_limit do
+ min(Pleroma.Config.get([:instance, :limit]), Pleroma.Config.get([:instance, :remote_limit]))
+ end
+
+ defp normalize_source(%{"source" => source} = object) when is_binary(source) do
+ object
+ |> Map.put("source", %{"content" => source})
+ |> normalize_source()
+ end
+
+ defp normalize_source(%{"source" => source} = object) when is_map(source) do
+ source =
+ case source["content"] do
+ content when is_binary(content) ->
+ if String.length(content) <= mfm_parse_limit() do
+ source
+ else
+ Map.delete(source, "content")
+ end
+
+ nil ->
+ source
+
+ _ ->
+ Map.delete(source, "content")
+ end
+
+ Map.put(object, "source", source)
+ end
+
+ defp normalize_source(object), do: object
+
+ defp fix_misskey_content(%{"htmlMfm" => true, "content" => content} = object)
+ when is_binary(content) do
+ Map.put(object, "content", HTML.filter_tags(content))
+ end
+
+ defp fix_misskey_content(%{"htmlMfm" => true} = object), do: object
+
+ defp fix_misskey_content(
+ %{"source" => %{"mediaType" => "text/x.misskeymarkdown", "content" => content}} = object
+ )
+ when is_binary(content) do
+ mention_handler = fn nick, buffer, opts, acc ->
+ remote_mention_resolver(object, nick, buffer, opts, acc)
+ end
+
+ {linked, _mentions, _tags} =
+ Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler)
+
+ Map.put(object, "content", linked)
+ end
+
+ defp fix_misskey_content(%{"source" => %{"mediaType" => "text/x.misskeymarkdown"}} = object),
+ do: scrub_content(object)
+
+ defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do
+ object
+ |> Map.put("source", %{
+ "content" => content,
+ "mediaType" => "text/x.misskeymarkdown"
+ })
+ |> Map.delete("_misskey_content")
+ |> fix_misskey_content()
+ end
+
+ defp fix_misskey_content(object), do: object
+
defp fix(data) do
data
|> CommonFixes.fix_actor()
@@ -88,6 +199,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|> fix_tag()
|> fix_replies()
|> fix_attachments()
+ |> normalize_source()
+ |> fix_misskey_content()
|> CommonFixes.fix_quote_url()
|> CommonFixes.fix_likes()
|> Transmogrifier.fix_emoji()
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
index 22cf0cc05..9b8580200 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex
@@ -32,6 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
quote bind_quoted: binding() do
field(:content, :string)
field(:contentMap, ObjectValidators.ContentLanguageMap)
+ field(:htmlMfm, :boolean)
field(:published, ObjectValidators.DateTime)
field(:updated, ObjectValidators.DateTime)
diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
index 21940f4f1..065c75910 100644
--- a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
@@ -28,6 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
end
field(:closed, ObjectValidators.DateTime)
+ field(:votersCount, :integer)
field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
field(:nonAnonymous, :boolean)
embeds_many(:anyOf, QuestionOptionsValidator)
diff --git a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
index aab90235f..4c0d9dff7 100644
--- a/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/update_validator.ex
@@ -75,15 +75,40 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
end
end
- # For remote Updates, verify the host is the same.
+ # For remote Updates, verify the Actor is the same
def validate_updating_rights_remote(cng) do
with actor = get_field(cng, :actor),
object = get_field(cng, :object),
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
- actor_uri <- URI.parse(actor),
- object_uri <- URI.parse(object_id),
- true <- actor_uri.host == object_uri.host do
- cng
+ entity <-
+ Object.normalize(object_id, fetch: false) || User.get_cached_by_ap_id(object_id) do
+ case entity do
+ # Actor must own Object to update it
+ %Object{} ->
+ if actor == entity.data["actor"] do
+ cng
+ else
+ cng
+ |> add_error(:object, "Can't be updated by this actor")
+ end
+
+ # Actor must only be allowed to update itself
+ %User{} ->
+ if actor == entity.ap_id do
+ cng
+ else
+ cng
+ |> add_error(:object, "Can't be updated by this actor")
+ end
+
+ nil ->
+ cng
+ |> add_error(:object, "Can't be updated by this actor")
+
+ _ ->
+ cng
+ |> add_error(:object, "Update is neither for Object or Actor")
+ end
else
_e ->
cng
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 6af5ee89d..a013dd226 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -430,6 +430,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end)
end
+ defp reject_third_party_report(%User{local: false}, %User{local: false} = account) do
+ {:reject, "[Transmogrifier] third-party report: #{account.ap_id}"}
+ end
+
+ defp reject_third_party_report(_, _), do: :ok
+
def handle_incoming(data, options \\ []) do
data
|> fix_recursive(&strip_internal_fields/1)
@@ -444,9 +450,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
) do
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
+ objects <- List.wrap(objects),
%User{} = actor <- User.get_cached_by_ap_id(actor),
# Reduce the object list to find the reported user.
%User{} = account <- get_reported(objects),
+ :ok <- reject_third_party_report(actor, account),
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
%{
@@ -783,6 +791,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def set_replies(obj_data), do: obj_data
+ defp set_voters_count(%{"voters" => [_ | _] = voters} = obj) do
+ Map.merge(obj, %{"votersCount" => length(voters)})
+ end
+
+ defp set_voters_count(obj), do: obj
+
# Prepares and sanitizes the object for federation.
def prepare_object(object) do
object
@@ -795,6 +809,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> set_reply_to_uri
|> set_quote_url
|> set_replies
+ |> set_voters_count
|> CommonFixes.maybe_add_content_map()
|> strip_internal_fields
|> strip_internal_tags
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 43c0f456d..0af4ceaf5 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -120,7 +120,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"https://www.w3.org/ns/activitystreams",
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
%{
- "@language" => get_language(data)
+ "@language" => get_language(data),
+ "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex
index d2e803178..87189edc2 100644
--- a/lib/pleroma/web/api_spec/operations/list_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/list_operation.ex
@@ -172,7 +172,10 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
type: :object,
properties: %{
title: %Schema{type: :string, description: "List title"},
- exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"}
+ exclusive: %Schema{
+ type: :boolean,
+ description: "Whether members of the list should be removed from the “Home” feed"
+ }
},
required: [:title]
},
@@ -188,7 +191,10 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
type: :object,
properties: %{
title: %Schema{type: :string, description: "List title"},
- exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"}
+ exclusive: %Schema{
+ type: :boolean,
+ description: "Whether members of the list should be removed from the “Home” feed"
+ }
}
},
required: true
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
index 16489663a..6072fff6b 100644
--- a/lib/pleroma/web/common_api/activity_draft.ex
+++ b/lib/pleroma/web/common_api/activity_draft.ex
@@ -317,6 +317,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
emoji = Map.merge(emoji, summary_emoji)
+ media_type = Utils.get_content_type(draft.params[:content_type])
{:ok, note_data, _meta} = Builder.note(draft)
object =
@@ -324,14 +325,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> Map.put("emoji", emoji)
|> Map.put("source", %{
"content" => draft.status,
- "mediaType" => Utils.get_content_type(draft.params[:content_type])
+ "mediaType" => media_type
})
+ |> maybe_put("htmlMfm", true, media_type == "text/x.misskeymarkdown")
|> Map.put("generator", draft.params[:generator])
|> Map.put("language", draft.language)
%{draft | object: object}
end
+ defp maybe_put(map, key, value, true), do: Map.put(map, key, value)
+ defp maybe_put(map, _key, _value, _condition), do: map
+
defp preview?(%__MODULE__{} = draft) do
preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
%{draft | preview?: preview?}
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 32572a721..26034d685 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -322,6 +322,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.linkify(options)
end
+ def format_input(text, "text/x.misskeymarkdown", options) do
+ text
+ |> Formatter.markdown_to_html(%{breaks: true})
+ |> safe_mfm_to_html()
+ |> Formatter.linkify(options)
+ |> Formatter.html_escape("text/x.misskeymarkdown")
+ end
+
def format_input(text, "text/markdown", options) do
text
|> Formatter.mentions_escape(options)
@@ -330,6 +338,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.html_escape("text/html")
end
+ defp safe_mfm_to_html(html) do
+ html
+ |> MfmParser.Parser.parse()
+ |> MfmParser.Encoder.to_html()
+ rescue
+ _ -> html
+ catch
+ _, _ -> html
+ end
+
def format_naive_asctime(date) do
date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
end
diff --git a/lib/pleroma/web/embed_controller.ex b/lib/pleroma/web/embed_controller.ex
index 2ca4501a6..8420f17a5 100644
--- a/lib/pleroma/web/embed_controller.ex
+++ b/lib/pleroma/web/embed_controller.ex
@@ -20,6 +20,7 @@ defmodule Pleroma.Web.EmbedController do
conn
|> delete_resp_header("x-frame-options")
|> delete_resp_header("content-security-policy")
+ |> put_layout({Pleroma.Web.LayoutView, :embed})
|> render("show.html",
activity: activity,
author: User.sanitize_html(author),
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index bab3c9fd0..a5c04a0c4 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -46,8 +46,10 @@ defmodule Pleroma.Web.Endpoint do
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
plug(Pleroma.Web.Plugs.UploadedMedia)
- @static_cache_control "public, max-age=1209600"
+ @static_cache_control "public, max-age=1209600, immutable"
@static_cache_disabled "public, no-cache"
+ # cache for a day
+ @favicon_cache_control "public, max=age=86400, immutable"
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
@@ -64,6 +66,15 @@ defmodule Pleroma.Web.Endpoint do
}
)
+ plug(Pleroma.Web.Plugs.FaviconPlug,
+ at: "/",
+ only: ["favicon.png"],
+ cache_control_for_etags: @favicon_cache_control,
+ headers: %{
+ "cache-control" => @favicon_cache_control
+ }
+ )
+
plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,
diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex
index d75a95fb3..c7f80ad77 100644
--- a/lib/pleroma/web/fallback/redirect_controller.ex
+++ b/lib/pleroma/web/fallback/redirect_controller.ex
@@ -29,9 +29,18 @@ defmodule Pleroma.Web.Fallback.RedirectController do
)
end
- def live_dashboard(conn, _params) do
+ def live_dashboard(conn, %{"path" => path}) do
+ query_params = conn.query_string
+
+ redirect_path =
+ if query_params == "" do
+ "/pleroma/live_dashboard/#{path}"
+ else
+ "/pleroma/live_dashboard/#{path}?#{query_params}"
+ end
+
conn
- |> redirect(to: "/pleroma/live_dashboard")
+ |> redirect(to: redirect_path)
end
def redirector(conn, _params, code \\ 200) do
diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex
index 676fc5137..90cd2e54a 100644
--- a/lib/pleroma/web/federator.ex
+++ b/lib/pleroma/web/federator.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Workers.PublisherWorker
alias Pleroma.Workers.ReceiverWorker
+ alias Pleroma.Workers.SignatureRetryWorker
require Logger
@@ -35,12 +36,21 @@ defmodule Pleroma.Web.Federator do
end
# Client API
- def incoming_ap_doc(%{params: params, req_headers: req_headers}) do
- ReceiverWorker.new(
+ def incoming_failed_signature_ap_doc(%{
+ method: method,
+ params: params,
+ req_headers: req_headers,
+ request_path: request_path,
+ query_string: query_string
+ }) do
+ SignatureRetryWorker.new(
%{
- "op" => "incoming_ap_doc",
+ "op" => "incoming_failed_signature_ap_doc",
+ "method" => method,
"req_headers" => req_headers,
"params" => params,
+ "request_path" => request_path,
+ "query_string" => query_string,
"timeout" => :timer.seconds(20)
},
priority: 2
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
index 1e3c9f36d..f047804e2 100644
--- a/lib/pleroma/web/mastodon_api/views/poll_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -71,6 +71,8 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
end)
end
+ defp voters_count(%{data: %{"votersCount" => voters}}) when is_integer(voters), do: voters
+
defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do
length(voters)
end
diff --git a/lib/pleroma/web/o_auth/token_controller.ex b/lib/pleroma/web/o_auth/token_controller.ex
index 37d08eea3..d2313a9cc 100644
--- a/lib/pleroma/web/o_auth/token_controller.ex
+++ b/lib/pleroma/web/o_auth/token_controller.ex
@@ -7,8 +7,8 @@ defmodule Pleroma.Web.OAuth.TokenController do
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
- alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.OAuth.TokenView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger
diff --git a/lib/pleroma/web/pleroma_api/controllers/token_controller.ex b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex
index 581dd569a..10eeb907e 100644
--- a/lib/pleroma/web/pleroma_api/controllers/token_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex
@@ -7,8 +7,8 @@ defmodule Pleroma.Web.PleromaAPI.TokenController do
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
- alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.PleromaAPI.TokenView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger
diff --git a/lib/pleroma/web/plugs/favicon_plug.ex b/lib/pleroma/web/plugs/favicon_plug.ex
new file mode 100644
index 000000000..e2e1f1adb
--- /dev/null
+++ b/lib/pleroma/web/plugs/favicon_plug.ex
@@ -0,0 +1,71 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2026 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.FaviconPlug do
+ @behaviour Plug
+
+ @moduledoc """
+ This is a shim to call `Plug.Static` but with runtime `from` configuration for instance favicon.
+
+ Serves default or custom favicon.png with cacheable cache-control.
+ """
+
+ import Plug.Conn, only: [put_resp_header: 3, send_resp: 3, halt: 1]
+
+ require Logger
+
+ def init(opts) do
+ opts
+ |> Keyword.put(:from, "__unconfigured_favicon_static_plug")
+ |> Plug.Static.init()
+ end
+
+ def call(%{request_path: "/favicon.png"} = conn, opts) do
+ case find_favicon_dir() do
+ {:ok, dir} ->
+ call_static(conn, opts, dir)
+
+ :error ->
+ # Favicon should always be available and this should never occur.
+ # If it does, halt the pipeline before having unintended side-effects.
+ Logger.error("No favicon.png found! Is the default favicon deleted?")
+
+ conn
+ |> send_resp(404, "Not found")
+ |> halt()
+ end
+ end
+
+ def call(conn, _) do
+ conn
+ end
+
+ defp find_favicon_dir do
+ instance_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static")
+ instance_path = Path.join(instance_dir, "favicon.png")
+
+ priv_dir = Application.app_dir(:pleroma, "priv/static")
+ priv_path = Path.join(priv_dir, "favicon.png")
+
+ cond do
+ File.exists?(instance_path) -> {:ok, instance_dir}
+ File.exists?(priv_path) -> {:ok, priv_dir}
+ true -> :error
+ end
+ end
+
+ defp call_static(conn, opts, from) do
+ opts =
+ opts
+ |> Map.put(:from, from)
+ |> Map.put(:content_types, false)
+
+ conn = set_content_type(conn)
+ Plug.Static.call(conn, opts)
+ end
+
+ defp set_content_type(conn) do
+ put_resp_header(conn, "content-type", "image/png")
+ end
+end
diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex
index d2a674d39..948188287 100644
--- a/lib/pleroma/web/plugs/instance_static.ex
+++ b/lib/pleroma/web/plugs/instance_static.ex
@@ -4,7 +4,7 @@
defmodule Pleroma.Web.Plugs.InstanceStatic do
require Pleroma.Constants
- import Plug.Conn, only: [put_resp_header: 3]
+ import Plug.Conn, only: [put_resp_header: 3, put_status: 2, send_resp: 3, halt: 1]
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration.
@@ -51,10 +51,37 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do
|> Map.put(:from, from)
|> Map.put(:content_types, false)
- conn = set_content_type(conn, conn.request_path)
+ conn =
+ conn
+ |> set_content_type(conn.request_path)
+ |> Plug.Static.call(opts)
- # Call Plug.Static with our sanitized content-type
- Plug.Static.call(conn, opts)
+ if conn.halted do
+ conn
+ else
+ path = String.trim_leading(conn.request_path, "/")
+
+ if not File.exists?(file_path(path)) do
+ conn
+ |> put_status(:not_found)
+ |> send_404()
+ |> halt()
+ else
+ conn
+ end
+ end
+ end
+
+ defp send_404(conn) do
+ if String.ends_with?(String.downcase(conn.request_path), ".json") do
+ conn
+ |> put_resp_header("content-type", "application/json")
+ |> send_resp(404, Jason.encode!(%{error: "not found"}))
+ else
+ conn
+ |> put_resp_header("content-type", "text/plain")
+ |> send_resp(404, "Not found")
+ end
end
defp set_content_type(conn, "/emoji/" <> filepath) do
diff --git a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
index c6d531086..a688b1780 100644
--- a/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
+++ b/lib/pleroma/web/plugs/mapped_signature_to_identity_plug.ex
@@ -32,8 +32,8 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
# remove me once testsuite uses mapped capabilities instead of what we do now
{:user, nil} ->
Logger.debug("Failed to map identity from signature (lookup failure)")
- Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
- conn
+ Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
+ assign(conn, :valid_signature, false)
end
end
diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex
index aa79dbf6b..0ebb73bbc 100644
--- a/lib/pleroma/web/plugs/rate_limiter.ex
+++ b/lib/pleroma/web/plugs/rate_limiter.ex
@@ -67,6 +67,8 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
import Plug.Conn
alias Pleroma.Config
+ alias Pleroma.Config.Holder
+ alias Pleroma.EctoType.Config.RateLimit
alias Pleroma.User
alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor
@@ -143,7 +145,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
def action_settings(plug_opts) do
with limiter_name when is_atom(limiter_name) <- plug_opts[:name],
- limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do
+ {:ok, limits} <- fetch_and_normalize_limits(limiter_name) do
bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name)
%{
@@ -151,6 +153,72 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
limits: limits,
opts: plug_opts
}
+ else
+ :disabled -> nil
+ end
+ end
+
+ defp fetch_and_normalize_limits(limiter_name) do
+ limits = Config.get([:rate_limit, limiter_name])
+
+ case normalize_limits(limits) do
+ {:ok, limits} ->
+ {:ok, limits}
+
+ :disabled ->
+ :disabled
+
+ :error ->
+ default_limits =
+ Holder.default_config(:pleroma, :rate_limit)
+ |> get_default_limits(limiter_name)
+
+ case normalize_limits(default_limits) do
+ {:ok, normalized_limits} ->
+ warn_invalid_limits_once(limiter_name, limits)
+ {:ok, normalized_limits}
+
+ _ ->
+ warn_invalid_limits_once(limiter_name, limits)
+ :disabled
+ end
+ end
+ end
+
+ defp get_default_limits(%{} = rate_limit, limiter_name), do: Map.get(rate_limit, limiter_name)
+
+ defp get_default_limits(rate_limit, limiter_name) when is_list(rate_limit) do
+ if Keyword.keyword?(rate_limit) do
+ Keyword.get(rate_limit, limiter_name)
+ else
+ nil
+ end
+ end
+
+ defp get_default_limits(_, _), do: nil
+
+ @invalid_limits_warned_key {__MODULE__, :invalid_limits_warned}
+
+ defp warn_invalid_limits_once(limiter_name, limits) do
+ warned = :persistent_term.get(@invalid_limits_warned_key, MapSet.new())
+
+ if MapSet.member?(warned, limiter_name) do
+ :ok
+ else
+ :persistent_term.put(@invalid_limits_warned_key, MapSet.put(warned, limiter_name))
+
+ Logger.warning(
+ "Invalid rate limiter config for #{inspect(limiter_name)}: #{inspect(limits)}. Falling back to defaults or disabling this limiter."
+ )
+ end
+ end
+
+ defp normalize_limits(nil), do: :disabled
+
+ defp normalize_limits(limits) do
+ case RateLimit.cast(limits) do
+ {:ok, normalized_limits} -> {:ok, normalized_limits}
+ :error -> :error
end
end
diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex
index abacf965b..5a3ea12aa 100644
--- a/lib/pleroma/web/plugs/uploaded_media.ex
+++ b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
# no slashes
@path "media"
- @default_cache_control_header "public, max-age=1209600"
+ @default_cache_control_header "public, max-age=1209600, immutable"
def init(_opts) do
static_plug_opts =
diff --git a/lib/pleroma/web/preload/providers/instance.ex b/lib/pleroma/web/preload/providers/instance.ex
index d5417af30..f2a60393f 100644
--- a/lib/pleroma/web/preload/providers/instance.ex
+++ b/lib/pleroma/web/preload/providers/instance.ex
@@ -5,9 +5,9 @@
defmodule Pleroma.Web.Preload.Providers.Instance do
alias Pleroma.Web.MastodonAPI.InstanceView
alias Pleroma.Web.Nodeinfo.Nodeinfo
+ alias Pleroma.Web.PleromaAPI.UtilView
alias Pleroma.Web.Plugs.InstanceStatic
alias Pleroma.Web.Preload.Providers.Provider
- alias Pleroma.Web.PleromaAPI.UtilView
@behaviour Provider
@instance_url "/api/v1/instance"
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index eea0a0912..c91ca8c97 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1032,6 +1032,8 @@ defmodule Pleroma.Web.Router do
pipe_through(:pleroma_html)
post("/auth/password", OAuth.PasswordController, :request)
+
+ get("/embed/:id", EmbedController, :show)
end
scope "/proxy/", Pleroma.Web do
@@ -1093,7 +1095,7 @@ defmodule Pleroma.Web.Router do
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
match(:*, "/api/pleroma/*path", LegacyPleromaApiRerouterPlug, [])
get("/api/*path", RedirectController, :api_not_implemented)
- get("/phoenix/live_dashboard", RedirectController, :live_dashboard)
+ get("/phoenix/live_dashboard/*path", RedirectController, :live_dashboard)
get("/*path", RedirectController, :redirector_with_preload)
options("/*path", RedirectController, :empty)
diff --git a/lib/pleroma/workers/cron/digest_emails_worker.ex b/lib/pleroma/workers/cron/digest_emails_worker.ex
index b50b52a7b..5cb13f5f9 100644
--- a/lib/pleroma/workers/cron/digest_emails_worker.ex
+++ b/lib/pleroma/workers/cron/digest_emails_worker.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorker do
The worker to send digest emails.
"""
- use Oban.Worker, queue: "mailer"
+ use Oban.Worker, queue: :background
alias Pleroma.Config
alias Pleroma.Emails
diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex
index e2c950967..3afbe138d 100644
--- a/lib/pleroma/workers/receiver_worker.ex
+++ b/lib/pleroma/workers/receiver_worker.ex
@@ -4,40 +4,37 @@
defmodule Pleroma.Workers.ReceiverWorker do
alias Pleroma.Instances
- alias Pleroma.Signature
- alias Pleroma.User
alias Pleroma.Web.Federator
+ alias Pleroma.Workers.SignatureRetryWorker
use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
@impl true
+ def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params} = args} = job) do
+ if signature_retry_job?(args) do
+ perform_signature_retry(job)
+ else
+ perform_incoming(params)
+ end
+ end
- def perform(%Job{
- args: %{
- "op" => "incoming_ap_doc",
- "method" => method,
- "params" => params,
- "req_headers" => req_headers,
- "request_path" => request_path,
- "query_string" => query_string
- }
- }) do
- # Oban's serialization converts our tuple headers to lists.
- # Revert it for the signature validation.
- req_headers = Enum.into(req_headers, [], &List.to_tuple(&1))
+ def perform(%Job{args: %{"op" => "incoming_ap_doc"} = args} = job) do
+ if signature_retry_job?(args) do
+ perform_signature_retry(job)
+ else
+ process_errors(:missing_incoming_ap_doc_params)
+ end
+ end
- conn_data = %Plug.Conn{
- method: method,
- params: params,
- req_headers: req_headers,
- request_path: request_path,
- query_string: query_string
- }
+ defp perform_signature_retry(%Job{args: args} = job) do
+ SignatureRetryWorker.perform(%Job{
+ job
+ | args: Map.put(args, "op", "incoming_failed_signature_ap_doc")
+ })
+ end
- with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]),
- {:ok, _public_key} <- Signature.refetch_public_key(conn_data),
- {:signature, true} <- {:signature, Signature.validate_signature(conn_data)},
- {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
+ defp perform_incoming(params) do
+ with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
unless Instances.reachable?(params["actor"]) do
domain = URI.parse(params["actor"]).host
Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
@@ -49,17 +46,8 @@ defmodule Pleroma.Workers.ReceiverWorker do
end
end
- def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do
- with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
- unless Instances.reachable?(params["actor"]) do
- domain = URI.parse(params["actor"]).host
- Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
- end
-
- {:ok, res}
- else
- e -> process_errors(e)
- end
+ defp signature_retry_job?(args) do
+ Enum.any?(~w(method req_headers request_path query_string), &Map.has_key?(args, &1))
end
@impl true
@@ -85,10 +73,12 @@ defmodule Pleroma.Workers.ReceiverWorker do
{:error, {:reject, _} = reason} -> {:cancel, reason}
# HTTP Sigs
{:signature, false} -> {:cancel, :invalid_signature}
+ {:same_actor, false} -> {:cancel, :actor_signature_mismatch}
# Origin / URL validation failed somewhere possibly due to spoofing
{:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
# Unclear if this can be reached
{:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason}
+ :missing_incoming_ap_doc_params -> {:cancel, :missing_incoming_ap_doc_params}
# Catchall
{:error, _} = e -> e
e -> {:error, e}
diff --git a/lib/pleroma/workers/signature_retry_worker.ex b/lib/pleroma/workers/signature_retry_worker.ex
new file mode 100644
index 000000000..2c4c097dd
--- /dev/null
+++ b/lib/pleroma/workers/signature_retry_worker.ex
@@ -0,0 +1,254 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.SignatureRetryWorker do
+ alias Pleroma.Instances
+ alias Pleroma.Signature
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.Federator
+ alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug
+
+ require Logger
+
+ use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
+
+ @impl true
+ def perform(%Job{
+ args: %{
+ "op" => "incoming_failed_signature_ap_doc",
+ "method" => method,
+ "params" => params,
+ "req_headers" => req_headers,
+ "request_path" => request_path,
+ "query_string" => query_string
+ }
+ })
+ when is_binary(method) and is_map(params) and is_list(req_headers) and
+ is_binary(request_path) and is_binary(query_string) do
+ case normalize_req_headers(req_headers) do
+ {:ok, req_headers} ->
+ conn_data = %Plug.Conn{
+ assigns: %{valid_signature: true},
+ method: method,
+ params: params,
+ req_headers: req_headers,
+ request_path: request_path,
+ query_string: query_string
+ }
+
+ signature_actor_result = signature_actor_id(conn_data)
+
+ with actor_id = Utils.get_ap_id(params["actor"]),
+ {:signature_actor, {:ok, signature_actor_id}} <-
+ {:signature_actor, signature_actor_result},
+ {:same_actor, true} <- {:same_actor, signature_actor_id == actor_id},
+ {:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id),
+ {:ok, _public_key} <- Signature.refetch_public_key(conn_data),
+ {:signature, true} <- {:signature, validate_signature(conn_data)},
+ {:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)},
+ {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
+ unless Instances.reachable?(params["actor"]) do
+ domain = URI.parse(params["actor"]).host
+ Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
+ end
+
+ {:ok, res}
+ else
+ e -> process_errors(e, retry_log_context(params, request_path, signature_actor_result))
+ end
+
+ e ->
+ process_errors(e, retry_log_context(params, request_path, nil))
+ end
+ end
+
+ def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"} = args}) do
+ process_errors(
+ :missing_signature_retry_metadata,
+ retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil)
+ )
+ end
+
+ def perform(%Job{args: args}) when is_map(args) do
+ process_errors(
+ :missing_signature_retry_metadata,
+ retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil)
+ )
+ end
+
+ def perform(%Job{}), do: process_errors(:missing_signature_retry_metadata)
+
+ @impl true
+ def timeout(%_{args: %{"timeout" => timeout}}), do: timeout
+
+ def timeout(_job), do: :timer.seconds(5)
+
+ defp normalize_req_headers(req_headers) do
+ req_headers
+ |> Enum.reduce_while({:ok, []}, fn
+ {key, value}, {:ok, acc} when is_binary(key) and is_binary(value) ->
+ {:cont, {:ok, [{key, value} | acc]}}
+
+ [key, value], {:ok, acc} when is_binary(key) and is_binary(value) ->
+ {:cont, {:ok, [{key, value} | acc]}}
+
+ _, _ ->
+ {:halt, {:error, :invalid_signature_retry_metadata}}
+ end)
+ |> case do
+ {:ok, headers} -> {:ok, Enum.reverse(headers)}
+ error -> error
+ end
+ end
+
+ defp validate_same_actor(conn_data) do
+ case MappedSignatureToIdentityPlug.call(conn_data, []) do
+ %Plug.Conn{assigns: %{valid_signature: true}} ->
+ true
+
+ _ ->
+ false
+ end
+ end
+
+ defp validate_signature(conn_data) do
+ Signature.validate_signature(conn_data)
+ rescue
+ _ -> false
+ catch
+ _, _ -> false
+ end
+
+ defp signature_actor_id(conn_data) do
+ Signature.get_actor_id(conn_data)
+ rescue
+ _ -> {:error, :invalid_signature}
+ catch
+ _, _ -> {:error, :invalid_signature}
+ end
+
+ defp process_errors(errors, context \\ %{})
+
+ defp process_errors({:error, {:error, _} = error}, context), do: process_errors(error, context)
+
+ defp process_errors(errors, context) do
+ result =
+ case errors do
+ # User fetch failures
+ {:error, :not_found} = reason ->
+ {:cancel, reason}
+
+ {:error, :forbidden} = reason ->
+ {:cancel, reason}
+
+ # Inactive user
+ {:error, {:user_active, false} = reason} ->
+ {:cancel, reason}
+
+ # Validator will error and return a changeset error
+ # e.g., duplicate activities or if the object was deleted
+ {:error, {:validate, {:error, _changeset} = reason}} ->
+ {:cancel, reason}
+
+ # Duplicate detection during Normalization
+ {:error, :already_present} ->
+ {:cancel, :already_present}
+
+ # MRFs will return a reject
+ {:error, {:reject, _} = reason} ->
+ {:cancel, reason}
+
+ # HTTP Sigs
+ {:signature_actor, {:error, _}} ->
+ {:cancel, :invalid_signature}
+
+ {:signature, false} ->
+ {:cancel, :invalid_signature}
+
+ {:same_actor, false} ->
+ {:cancel, :actor_signature_mismatch}
+
+ # Origin / URL validation failed somewhere possibly due to spoofing
+ {:error, :origin_containment_failed} ->
+ {:cancel, :origin_containment_failed}
+
+ # Unclear if this can be reached
+ {:error, {:side_effects, {:error, :no_object_actor}} = reason} ->
+ {:cancel, reason}
+
+ # Fail closed if the retry cannot reconstruct the original request.
+ :missing_signature_retry_metadata ->
+ {:cancel, :missing_signature_retry_metadata}
+
+ {:error, :invalid_signature_retry_metadata} ->
+ {:cancel, :invalid_signature_retry_metadata}
+
+ # Catchall
+ {:error, _} = e ->
+ e
+
+ e ->
+ {:error, e}
+ end
+
+ log_signature_retry_rejection(result, context)
+ result
+ end
+
+ defp retry_log_context(params, request_path, signature_actor_result) when is_map(params) do
+ signature_actor =
+ case signature_actor_result do
+ {:ok, actor} when is_binary(actor) -> actor
+ actor when is_binary(actor) -> actor
+ _ -> nil
+ end
+
+ %{
+ activity_id: params["id"],
+ payload_actor: Utils.get_ap_id(params["actor"]),
+ request_path: request_path,
+ signature_actor: signature_actor,
+ type: params["type"]
+ }
+ end
+
+ defp retry_log_context(_params, request_path, signature_actor_result) do
+ signature_actor =
+ case signature_actor_result do
+ {:ok, actor} when is_binary(actor) -> actor
+ actor when is_binary(actor) -> actor
+ _ -> nil
+ end
+
+ %{
+ activity_id: nil,
+ payload_actor: nil,
+ request_path: request_path,
+ signature_actor: signature_actor,
+ type: nil
+ }
+ end
+
+ defp log_signature_retry_rejection({:cancel, reason}, context)
+ when reason in [
+ :actor_signature_mismatch,
+ :invalid_signature,
+ :invalid_signature_retry_metadata,
+ :missing_signature_retry_metadata,
+ :origin_containment_failed
+ ] do
+ Logger.warning(
+ "Failed-signature inbox retry rejected " <>
+ "reason=#{inspect(reason)} " <>
+ "payload_actor=#{inspect(context[:payload_actor])} " <>
+ "signature_actor=#{inspect(context[:signature_actor])} " <>
+ "activity_id=#{inspect(context[:activity_id])} " <>
+ "type=#{inspect(context[:type])} " <>
+ "request_path=#{inspect(context[:request_path])}"
+ )
+ end
+
+ defp log_signature_retry_rejection(_result, _context), do: :ok
+end
diff --git a/mix.exs b/mix.exs
index 60064e846..7a67b54a7 100644
--- a/mix.exs
+++ b/mix.exs
@@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do
[
app: :pleroma,
- version: version("2.10.0"),
+ version: version("2.10.2"),
elixir: "~> 1.15",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(),
@@ -144,9 +144,7 @@ defmodule Pleroma.Mixfile do
{:tzdata, "~> 1.0.5"},
{:plug_cowboy, "~> 2.7"},
{:oban, "~> 2.19.4"},
- {:oban_plugins_lazarus,
- git: "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git",
- ref: "e49fc355baaf0e435208bf5f534d31e26e897711"},
+ {:oban_plugins_lazarus, "~> 0.1.1"},
{:oban_web, "~> 2.11"},
{:gettext, "~> 0.24"},
{:bcrypt_elixir, "~> 2.3"},
@@ -167,6 +165,7 @@ defmodule Pleroma.Mixfile do
{:sweet_xml, "~> 0.7.5"},
{:earmark, "1.4.46"},
{:bbcode_pleroma, "~> 0.2.0"},
+ {:pleroma_mfm_parser, "~> 0.2.1"},
{:cors_plug, "~> 2.0"},
{:web_push_encryption, "~> 0.3.1"},
{:swoosh, "~> 1.16.12"},
@@ -179,7 +178,7 @@ defmodule Pleroma.Mixfile do
{:timex, "~> 3.7"},
{:ueberauth, "~> 0.10"},
{:linkify, "~> 0.5.3"},
- {:http_signatures, "~> 0.1.2"},
+ {:http_signatures, "~> 0.1.3"},
{:telemetry, "~> 1.0.0", override: true},
{:poolboy, "~> 1.5"},
{:prom_ex, "~> 1.9"},
@@ -197,14 +196,14 @@ defmodule Pleroma.Mixfile do
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"},
{:restarter, path: "./restarter"},
- {:majic, "~> 1.1"},
+ {:majic, "~> 1.2"},
{:open_api_spex, "~> 3.22"},
{:ecto_psql_extras, "~> 0.8"},
{:vix, "~> 0.36"},
{:elixir_make, "~> 0.7.8", override: true},
{:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash},
{:exile, "~> 0.10.0"},
- {:bandit, "~> 1.5.7"},
+ {:bandit, "~> 1.10"},
{:websock_adapter, "~> 0.5.8"},
{:oban_live_dashboard, "~> 0.1.1"},
{:multipart, "~> 0.4.0", optional: true},
@@ -218,7 +217,7 @@ defmodule Pleroma.Mixfile do
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.9", only: :test},
{:covertool, "~> 2.0", only: :test},
- {:hackney, "~> 1.25.0", override: true},
+ {:hackney, "~> 1.20.1", override: true},
{:mox, "~> 1.2", only: :test},
{:websockex, "~> 0.4.3", only: :test},
{:benchee, "~> 1.4", only: :benchmark},
diff --git a/mix.lock b/mix.lock
index 8e1f684dc..f33c22482 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,7 +1,7 @@
%{
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"},
"argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"},
- "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
+ "bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"},
"base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"},
"bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"},
@@ -13,7 +13,7 @@
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e7b7cc34cc16b383461b966484c297e4ec9aeef6", [ref: "e7b7cc34cc16b383461b966484c297e4ec9aeef6"]},
"castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
- "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
+ "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
"concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"},
@@ -59,10 +59,10 @@
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"},
- "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
+ "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
- "http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"},
+ "http_signatures": {:hex, :http_signatures, "0.1.3", "19f26aee35b4684e37efdce3ac4638605e6e41a04368186bd39d2b6138a60ea9", [:mix], [{:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20313a65516db88006f85b090f6f76cc5b04e9609b45943657e6781eb91174f4"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"},
@@ -73,14 +73,14 @@
"linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"},
"logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"},
"mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"},
- "majic": {:hex, :majic, "1.1.1", "16092a3a3376cc5e13d207e82ec06e05a5561170465e50cc11cc4df8a5747938", [:make, :mix], [{:elixir_make, "~> 0.8.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7fbb0372f0447b3f777056177d6ab3f009742e68474f850521ff56b84bd85b96"},
+ "majic": {:hex, :majic, "1.2.0", "414b69c1460ece692fa892a0ed6669b8db6a44c42bb3071cb723854f22e7bd78", [:make, :mix], [{:elixir_make, "~> 0.8.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0bd0c65f8e4dcc595757d957577f6151b59246da9614ba87debfdc641ccc0514"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
- "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
+ "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"},
@@ -96,7 +96,7 @@
"oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"},
"oban_live_dashboard": {:hex, :oban_live_dashboard, "0.1.1", "8aa4ceaf381c818f7d5c8185cc59942b8ac82ef0cf559881aacf8d3f8ac7bdd3", [:mix], [{:oban, "~> 2.15", [hex: :oban, repo: "hexpm", optional: false]}, {:phoenix_live_dashboard, "~> 0.7", [hex: :phoenix_live_dashboard, repo: "hexpm", optional: false]}], "hexpm", "16dc4ce9c9a95aa2e655e35ed4e675652994a8def61731a18af85e230e1caa63"},
"oban_met": {:hex, :oban_met, "1.0.5", "bb633ab06448dab2ef9194f6688d33b3d07fc3f2ad793a1a08f4dfbb2cc9fe50", [:mix], [{:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "64664d50805bbfd3903aeada1f3c39634652a87844797ee400b0bcc95a28f5ea"},
- "oban_plugins_lazarus": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/oban_plugins_lazarus.git", "e49fc355baaf0e435208bf5f534d31e26e897711", [ref: "e49fc355baaf0e435208bf5f534d31e26e897711"]},
+ "oban_plugins_lazarus": {:hex, :oban_plugins_lazarus, "0.1.1", "a5141d569e5b9f3bec8f4a958231d2c538af097d3c1ad06274f096fc06956821", [:mix], [{:oban, "< 3.0.0", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "7e9c51bc44d33f71c0a52cf0cd5c8d4c70380ede065c1042013f5123f2fc9729"},
"oban_web": {:hex, :oban_web, "2.11.6", "53933cb4253c4d9f1098ee311c06f07935259f0e564dcf2d66bae4cc98e317fe", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.0", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "576d94b705688c313694c2c114ca21aa0f8f2ad1b9ca45c052c5ba316d3e8d10"},
"octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"},
"open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"},
@@ -112,6 +112,7 @@
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
+ "pleroma_mfm_parser": {:hex, :pleroma_mfm_parser, "0.2.1", "87a40fec4e2e035447df34e1c98e1b816b6a448d49baea73eb2273531ac5ce66", [:mix], [], "hexpm", "ec4845461da95d63ab3d592086d65114c45057a1f2d48d768f3def45b0d7696f"},
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
@@ -128,6 +129,7 @@
"prometheus_phx": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/prometheus-phx.git", "9cd8f248c9381ffedc799905050abce194a97514", [branch: "no-logging"]},
"prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"},
"quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"},
+ "quic": {:hex, :quic, "0.10.2", "4b390507a85f65ce47808f3df1a864e0baf9adb7a1b4ea9f4dcd66fe9d0cb166", [:rebar3], [], "hexpm", "7c196a66973c877a59768a5687f0a0610ff11817254d0a4e45cc4e3a16b1d00b"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
"remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]},
@@ -144,7 +146,7 @@
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
"tesla": {:hex, :tesla, "1.15.3", "3a2b5c37f09629b8dcf5d028fbafc9143c0099753559d7fe567eaabfbd9b8663", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "98bb3d4558abc67b92fb7be4cd31bb57ca8d80792de26870d362974b58caeda7"},
- "thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"},
+ "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
diff --git a/priv/repo/migrations/20260401185429_cleanup_stale_digest_email_jobs.exs b/priv/repo/migrations/20260401185429_cleanup_stale_digest_email_jobs.exs
new file mode 100644
index 000000000..882c7ec66
--- /dev/null
+++ b/priv/repo/migrations/20260401185429_cleanup_stale_digest_email_jobs.exs
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.CleanupStaleDigestEmailJobs do
+ use Ecto.Migration
+
+ def up do
+ execute(
+ "DELETE from oban_jobs WHERE queue = 'mailer' AND worker = 'Pleroma.Workers.Cron.DigestEmailsWorker'"
+ )
+ end
+end
diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex
index 0defdc74e..342ef9944 100644
--- a/priv/scrubbers/default.ex
+++ b/priv/scrubbers/default.ex
@@ -82,12 +82,50 @@ defmodule Pleroma.HTML.Scrubber.Default do
"recipients-inline",
"quote-inline",
"invisible",
- "ellipsis"
+ "ellipsis",
+ "mfm-center",
+ "mfm-flip",
+ "mfm-font",
+ "mfm-blur",
+ "mfm-rotate",
+ "mfm-x2",
+ "mfm-x3",
+ "mfm-x4",
+ "mfm-position",
+ "mfm-scale",
+ "mfm-fg",
+ "mfm-bg",
+ "mfm-jelly",
+ "mfm-twitch",
+ "mfm-shake",
+ "mfm-spin",
+ "mfm-jump",
+ "mfm-bounce",
+ "mfm-rainbow",
+ "mfm-tada",
+ "mfm-sparkle"
])
Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"])
- Meta.allow_tag_with_these_attributes(:span, ["lang"])
+ Meta.allow_tag_with_these_attributes(:span, [
+ "lang",
+ "data-mfm-h",
+ "data-mfm-v",
+ "data-mfm-x",
+ "data-mfm-y",
+ "data-mfm-alternate",
+ "data-mfm-speed",
+ "data-mfm-deg",
+ "data-mfm-left",
+ "data-mfm-serif",
+ "data-mfm-monospace",
+ "data-mfm-cursive",
+ "data-mfm-fantasy",
+ "data-mfm-emoji",
+ "data-mfm-math",
+ "data-mfm-color"
+ ])
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index 3569165a4..4e82d4b01 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -4,46 +4,55 @@
"https://w3id.org/security/v1",
"https://purl.archive.org/socialweb/webfinger",
{
- "Emoji": "toot:Emoji",
+ "as": "https://www.w3.org/ns/activitystreams#",
+ "ostatus": "http://ostatus.org#",
+ "schema": "http://schema.org#",
+ "vcard": "http://www.w3.org/2006/vcard/ns#",
+
+ "fedibird": "http://fedibird.com/ns#",
+ "litepub": "http://litepub.social/ns#",
+ "sm": "http://smithereen.software/ns#",
+ "toot": "http://joinmastodon.org/ns#",
+
+ "alsoKnownAs": {
+ "@id": "as:alsoKnownAs",
+ "@type": "@id"
+ },
"Hashtag": "as:Hashtag",
- "PropertyValue": "schema:PropertyValue",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "quoteUrl": "as:quoteUrl",
+ "sensitive": "as:sensitive",
+
"atomUri": "ostatus:atomUri",
"conversation": {
"@id": "ostatus:conversation",
"@type": "@id"
},
- "discoverable": "toot:discoverable",
- "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
- "capabilities": "litepub:capabilities",
- "ostatus": "http://ostatus.org#",
- "schema": "http://schema.org#",
- "toot": "http://joinmastodon.org/ns#",
- "fedibird": "http://fedibird.com/ns#",
+
+ "PropertyValue": "schema:PropertyValue",
"value": "schema:value",
- "sensitive": "as:sensitive",
- "litepub": "http://litepub.social/ns#",
- "invisible": "litepub:invisible",
+
+ "quoteUri": "fedibird:quoteUri",
+
+ "capabilities": "litepub:capabilities",
+ "ChatMessage": "litepub:ChatMessage",
"directMessage": "litepub:directMessage",
+ "EmojiReact": "litepub:EmojiReact",
+ "formerRepresentations": "litepub:formerRepresentations",
+ "invisible": "litepub:invisible",
"listMessage": {
"@id": "litepub:listMessage",
"@type": "@id"
},
- "quoteUrl": "as:quoteUrl",
- "quoteUri": "fedibird:quoteUri",
"oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id"
},
- "EmojiReact": "litepub:EmojiReact",
- "ChatMessage": "litepub:ChatMessage",
- "alsoKnownAs": {
- "@id": "as:alsoKnownAs",
- "@type": "@id"
- },
- "vcard": "http://www.w3.org/2006/vcard/ns#",
- "formerRepresentations": "litepub:formerRepresentations",
- "sm": "http://smithereen.software/ns#",
- "nonAnonymous": "sm:nonAnonymous"
+
+ "nonAnonymous": "sm:nonAnonymous",
+
+ "discoverable": "toot:discoverable",
+ "Emoji": "toot:Emoji"
}
]
-}
+}
\ No newline at end of file
diff --git a/rel/files/bin/pleroma_ctl b/rel/files/bin/pleroma_ctl
index 6f0dba3a8..f5d3f321b 100755
--- a/rel/files/bin/pleroma_ctl
+++ b/rel/files/bin/pleroma_ctl
@@ -78,14 +78,19 @@ update() {
RELEASE_ROOT=$(dirname "$SCRIPTPATH")
uri="https://git.pleroma.social"
- project_id="2"
- project_branch="${BRANCH:-$(detect_branch)}"
- flavour="${FLAVOUR:-$(detect_flavour)}"
+ project_name="pleroma"
+ package_base="${uri}/api/packages/${project_name}/generic"
+ if [ -n "$FULL_URI" ]; then
+ full_uri="$FULL_URI"
+ else
+ project_branch="${BRANCH:-$(detect_branch)}"
+ flavour="${FLAVOUR:-$(detect_flavour)}"
+ full_uri="${package_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/latest/pleroma.zip
+ fi
tmp="${TMP_DIR:-/tmp}"
artifact="$tmp/pleroma.zip"
- full_uri="${FULL_URI:-${uri}/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=${flavour}}"
echo "Downloading the artifact from ${full_uri} to ${artifact}"
- curl "$full_uri" -o "${artifact}"
+ curl -fL "$full_uri" -o "${artifact}"
echo "Unpacking ${artifact} to ${tmp}"
unzip -q "$artifact" -d "$tmp"
echo "Copying files over to $RELEASE_ROOT"
@@ -137,7 +142,14 @@ else
SCRIPT=$(realpath "$0")
SCRIPTPATH=$(dirname "$SCRIPT")
- FULL_ARGS="$*"
+ # HACK: Script arguments need to be sent as an array to Mix tasks, otherwise they will break (quoted) arguments with whitespace.
+ # Previously it was sent as string, which would get split on whitespace on the task side.
+ # Encode as Elixir binary literals to avoid string escaping and interpolation issues.
+ PREPARED_ARGS=""
+ for arg in "$@"; do
+ bytes=$(printf '%s' "$arg" | od -An -v -tu1 | tr -s '[:space:]' ',' | sed 's/^,//; s/,$//')
+ PREPARED_ARGS="$PREPARED_ARGS <<$bytes>>,"
+ done
ACTION="$1"
if [ $# -gt 0 ]; then
@@ -154,8 +166,8 @@ else
if [ "$ACTION" = "update" ]; then
update "$@"
elif [ "$ACTION" = "migrate" ] || [ "$ACTION" = "rollback" ] || [ "$ACTION" = "create" ] || [ "$ACTION $SUBACTION" = "instance gen" ] || [ "$PLEROMA_CTL_RPC_DISABLED" = true ]; then
- "$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")'
+ "$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run(['"${PREPARED_ARGS%%,}"'])'
else
- "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")'
+ "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run(['"${PREPARED_ARGS%%,}"'])'
fi
fi
diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs
index ef1adc235..f672d8c13 100644
--- a/test/mix/tasks/pleroma/config_test.exs
+++ b/test/mix/tasks/pleroma/config_test.exs
@@ -144,7 +144,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
- allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
+ allowed_post_formats: [
+ "text/plain",
+ "text/html",
+ "text/markdown",
+ "text/bbcode",
+ "text/x.misskeymarkdown"
+ ],
autofollowed_nicknames: [],
max_pinned_statuses: 1,
attachment_links: false,
diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs
index 98b56a146..9d70e6d54 100644
--- a/test/pleroma/config_db_test.exs
+++ b/test/pleroma/config_db_test.exs
@@ -174,6 +174,22 @@ defmodule Pleroma.ConfigDBTest do
assert updated1.value == [groups: [c: 3, d: 4], key: [a: 1, b: 2]]
assert updated2.value == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]]
end
+
+ test "rejects invalid :rate_limit values (e.g. empty-string scale from AdminFE)" do
+ assert {:error, _changeset} =
+ ConfigDB.update_or_create(%{
+ group: ":pleroma",
+ key: ":rate_limit",
+ value: [
+ %{
+ "tuple" => [
+ ":statuses_actions",
+ [%{"tuple" => ["", 0]}, %{"tuple" => ["", ""]}]
+ ]
+ }
+ ]
+ })
+ end
end
describe "delete/1" do
diff --git a/test/pleroma/dependency_version_test.exs b/test/pleroma/dependency_version_test.exs
new file mode 100644
index 000000000..0c9c1e939
--- /dev/null
+++ b/test/pleroma/dependency_version_test.exs
@@ -0,0 +1,16 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.DependencyVersionTest do
+ use ExUnit.Case, async: true
+
+ test "uses majic 1.2" do
+ majic_version =
+ :majic
+ |> Application.spec(:vsn)
+ |> to_string()
+
+ assert Version.match?(majic_version, "~> 1.2")
+ end
+end
diff --git a/test/pleroma/http/hackney_follow_redirect_regression_test.exs b/test/pleroma/http/hackney_follow_redirect_regression_test.exs
index 71fda4479..4fef26af6 100644
--- a/test/pleroma/http/hackney_follow_redirect_regression_test.exs
+++ b/test/pleroma/http/hackney_follow_redirect_regression_test.exs
@@ -88,6 +88,49 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
Pleroma.ReverseProxy.Client.Hackney.close(ref)
end
+ test "hackney reverse proxy follows nested redirects via proxy", %{
+ tls_server: tls_server,
+ proxy: proxy
+ } do
+ url = "#{tls_server.base_url}/nested_redirect"
+
+ opts = [
+ pool: :media,
+ proxy: proxy.proxy_url,
+ insecure: true,
+ connect_timeout: 1_000,
+ recv_timeout: 1_000,
+ follow_redirect: true
+ ]
+
+ assert {:ok, 200, _headers, ref} =
+ Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
+
+ assert collect_body(ref) == "ok"
+ Pleroma.ReverseProxy.Client.Hackney.close(ref)
+ end
+
+ test "hackney reverse proxy stops following redirects after limit is reached", %{
+ tls_server: tls_server,
+ proxy: proxy
+ } do
+ url = "#{tls_server.base_url}/infinite_redirect"
+
+ opts = [
+ pool: :media,
+ proxy: proxy.proxy_url,
+ insecure: true,
+ connect_timeout: 1_000,
+ recv_timeout: 1_000,
+ follow_redirect: true
+ ]
+
+ assert {:ok, 302, _headers, ref} =
+ Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
+
+ Pleroma.ReverseProxy.Client.Hackney.close(ref)
+ end
+
defp collect_body(ref, acc \\ "") do
case Pleroma.ReverseProxy.Client.Hackney.stream_body(ref) do
:done -> acc
@@ -148,6 +191,12 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
{:ok, data} <- recv_ssl_headers(ssl_socket),
{:ok, path} <- parse_path(data) do
case path do
+ "/infinite_redirect" ->
+ send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/infinite_redirect"}], "")
+
+ "/nested_redirect" ->
+ send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/redirect"}], "")
+
"/redirect" ->
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/final"}], "")
diff --git a/test/pleroma/pleroma_ctl_test.exs b/test/pleroma/pleroma_ctl_test.exs
new file mode 100644
index 000000000..d96396399
--- /dev/null
+++ b/test/pleroma/pleroma_ctl_test.exs
@@ -0,0 +1,293 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.PleromaCtlTest do
+ use ExUnit.Case, async: false
+
+ @pleroma_ctl Path.expand("../../rel/files/bin/pleroma_ctl", __DIR__)
+
+ setup do
+ tmp_dir =
+ Path.join(System.tmp_dir!(), "pleroma_ctl_test_#{System.unique_integer([:positive])}")
+
+ release_root = Path.join(tmp_dir, "release")
+ bin_dir = Path.join(release_root, "bin")
+
+ File.mkdir_p!(bin_dir)
+ File.cp!(@pleroma_ctl, Path.join(bin_dir, "pleroma_ctl"))
+ File.chmod!(Path.join(bin_dir, "pleroma_ctl"), 0o755)
+
+ on_exit(fn -> File.rm_rf!(tmp_dir) end)
+
+ {:ok, tmp_dir: tmp_dir, release_root: release_root, bin_dir: bin_dir}
+ end
+
+ test "update downloads branch-scoped latest OTP package", %{
+ tmp_dir: tmp_dir,
+ bin_dir: bin_dir,
+ release_root: release_root
+ } do
+ stubs_dir = Path.join(tmp_dir, "stubs")
+ curl_args_path = Path.join(tmp_dir, "curl.args")
+ File.mkdir_p!(stubs_dir)
+ write_update_stubs(stubs_dir, curl_args_path, "unused", "glibc")
+
+ update_tmp_dir = Path.join(tmp_dir, "update_tmp")
+ File.mkdir_p!(update_tmp_dir)
+
+ {output, status} =
+ System.cmd(
+ Path.join(bin_dir, "pleroma_ctl"),
+ [
+ "update",
+ "--branch",
+ "develop",
+ "--flavour",
+ "amd64",
+ "--tmp-dir",
+ update_tmp_dir,
+ "--no-rm"
+ ],
+ env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
+ stderr_to_stdout: true
+ )
+
+ assert status == 0, output
+ assert File.exists?(Path.join(release_root, "bin/marker"))
+
+ assert ["-fL", url, "-o", artifact_path] =
+ curl_args_path
+ |> File.read!()
+ |> String.split("\n", trim: true)
+
+ assert url ==
+ "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-amd64/latest/pleroma.zip"
+
+ assert artifact_path == Path.join(update_tmp_dir, "pleroma.zip")
+ end
+
+ test "update detects stable branch and local flavour", %{
+ tmp_dir: tmp_dir,
+ bin_dir: bin_dir,
+ release_root: release_root
+ } do
+ stubs_dir = Path.join(tmp_dir, "stubs")
+ curl_args_path = Path.join(tmp_dir, "curl.args")
+ File.mkdir_p!(stubs_dir)
+ write_update_stubs(stubs_dir, curl_args_path, "x86_64", "glibc")
+ write_start_erl_data(release_root, "2.10.0")
+
+ {output, status} =
+ System.cmd(
+ Path.join(bin_dir, "pleroma_ctl"),
+ ["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"],
+ env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
+ stderr_to_stdout: true
+ )
+
+ assert status == 0, output
+
+ assert curl_url(curl_args_path) ==
+ "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-amd64/latest/pleroma.zip"
+ end
+
+ test "update detects develop branch and musl arm flavour", %{
+ tmp_dir: tmp_dir,
+ bin_dir: bin_dir,
+ release_root: release_root
+ } do
+ stubs_dir = Path.join(tmp_dir, "stubs")
+ curl_args_path = Path.join(tmp_dir, "curl.args")
+ File.mkdir_p!(stubs_dir)
+ write_update_stubs(stubs_dir, curl_args_path, "armv7l", "musl")
+ write_start_erl_data(release_root, "2.10.0.develop")
+
+ {output, status} =
+ System.cmd(
+ Path.join(bin_dir, "pleroma_ctl"),
+ ["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"],
+ env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
+ stderr_to_stdout: true
+ )
+
+ assert status == 0, output
+
+ assert curl_url(curl_args_path) ==
+ "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-arm-musl/latest/pleroma.zip"
+ end
+
+ test "update with zip URL bypasses branch and flavour detection", %{
+ tmp_dir: tmp_dir,
+ bin_dir: bin_dir,
+ release_root: release_root
+ } do
+ stubs_dir = Path.join(tmp_dir, "stubs")
+ curl_args_path = Path.join(tmp_dir, "curl.args")
+ File.mkdir_p!(stubs_dir)
+ write_update_stubs(stubs_dir, curl_args_path, "unsupported-arch", "unsupported-libc")
+ write_start_erl_data(release_root, "2.10.0.custombranch")
+
+ custom_url = "https://example.test/custom.zip"
+
+ {output, status} =
+ System.cmd(
+ Path.join(bin_dir, "pleroma_ctl"),
+ [
+ "update",
+ "--zip-url",
+ custom_url,
+ "--tmp-dir",
+ create_update_tmp_dir(tmp_dir),
+ "--no-rm"
+ ],
+ env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
+ stderr_to_stdout: true
+ )
+
+ assert status == 0, output
+ assert curl_url(curl_args_path) == custom_url
+ end
+
+ test "passes arguments with spaces and Elixir string metacharacters", %{
+ tmp_dir: tmp_dir,
+ bin_dir: bin_dir
+ } do
+ capture_path = Path.join(tmp_dir, "captured_args")
+ eval_path = Path.join(tmp_dir, "pleroma_ctl_eval.exs")
+
+ write_executable(Path.join(bin_dir, "pleroma"), """
+ #!/bin/sh
+ {
+ printf '%s\n' 'defmodule Pleroma.ReleaseTasks do'
+ printf '%s\n' ' def run(args), do: File.write!(System.fetch_env!("PLEROMA_CTL_CAPTURE"), :erlang.term_to_binary(args))'
+ printf '%s\n' 'end'
+ printf '%s\n' "$2"
+ } > "$PLEROMA_CTL_EVAL_FILE"
+
+ exec elixir "$PLEROMA_CTL_EVAL_FILE"
+ """)
+
+ {output, status} =
+ System.cmd(
+ Path.join(bin_dir, "pleroma_ctl"),
+ [
+ "user",
+ "",
+ "has space",
+ ~s(has "quote"),
+ ~s(has \\ backslash),
+ ~S(#{:not_interpolated})
+ ],
+ env: [
+ {"PLEROMA_CTL_CAPTURE", capture_path},
+ {"PLEROMA_CTL_EVAL_FILE", eval_path}
+ ],
+ stderr_to_stdout: true
+ )
+
+ assert status == 0, output
+
+ assert capture_path
+ |> File.read!()
+ |> :erlang.binary_to_term() == [
+ "user",
+ "",
+ "has space",
+ ~s(has "quote"),
+ ~s(has \\ backslash),
+ ~S(#{:not_interpolated})
+ ]
+ end
+
+ defp write_executable(path, contents) do
+ File.write!(path, contents)
+ File.chmod!(path, 0o755)
+ end
+
+ defp write_start_erl_data(release_root, version) do
+ releases_dir = Path.join(release_root, "releases")
+ File.mkdir_p!(releases_dir)
+ File.write!(Path.join(releases_dir, "start_erl.data"), "erts-15.0 #{version}\n")
+ end
+
+ defp create_update_tmp_dir(tmp_dir) do
+ update_tmp_dir = Path.join(tmp_dir, "update_tmp")
+ File.mkdir_p!(update_tmp_dir)
+ update_tmp_dir
+ end
+
+ defp write_update_stubs(stubs_dir, curl_args_path, arch, libc) do
+ write_executable(Path.join(stubs_dir, "curl"), """
+ #!/bin/sh
+ printf '%s\n' "$@" > "#{curl_args_path}"
+
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ -o)
+ artifact="$2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+ done
+
+ : > "$artifact"
+ """)
+
+ write_executable(Path.join(stubs_dir, "unzip"), """
+ #!/bin/sh
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ -d)
+ dest="$2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+ done
+
+ mkdir -p "$dest/release/bin"
+ printf 'marker' > "$dest/release/bin/marker"
+ """)
+
+ write_executable(Path.join(stubs_dir, "uname"), """
+ #!/bin/sh
+ printf '%s\n' '#{arch}'
+ """)
+
+ write_executable(Path.join(stubs_dir, "getconf"), getconf_stub(libc))
+
+ write_executable(Path.join(stubs_dir, "ldd"), """
+ #!/bin/sh
+ printf '%s\n' 'musl libc (mock)'
+ """)
+ end
+
+ defp getconf_stub("glibc") do
+ """
+ #!/bin/sh
+ printf '%s\n' 'glibc 2.40'
+ """
+ end
+
+ defp getconf_stub(_libc) do
+ """
+ #!/bin/sh
+ exit 1
+ """
+ end
+
+ defp curl_url(curl_args_path) do
+ ["-fL", url, "-o", _artifact_path] =
+ curl_args_path
+ |> File.read!()
+ |> String.split("\n", trim: true)
+
+ url
+ end
+end
diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs
index 8dbe9c6bf..ec4470379 100644
--- a/test/pleroma/reverse_proxy_test.exs
+++ b/test/pleroma/reverse_proxy_test.exs
@@ -294,7 +294,7 @@ defmodule Pleroma.ReverseProxyTest do
|> expect(:stream_body, fn _ -> :done end)
conn = ReverseProxy.call(conn, "/cache")
- assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers
+ assert {"cache-control", "public, max-age=1209600, immutable"} in conn.resp_headers
end
end
diff --git a/test/pleroma/search/meilisearch_test.exs b/test/pleroma/search/meilisearch_test.exs
index eea454323..ff32491c5 100644
--- a/test/pleroma/search/meilisearch_test.exs
+++ b/test/pleroma/search/meilisearch_test.exs
@@ -74,29 +74,6 @@ defmodule Pleroma.Search.MeilisearchTest do
assert_received("posted_to_meilisearch")
end
- test "doesn't index posts that are not public" do
- user = insert(:user)
-
- Enum.each(["private", "direct"], fn visibility ->
- {:ok, activity} =
- CommonAPI.post(user, %{
- status: "guys i just don't wanna leave the swamp",
- visibility: visibility
- })
-
- args = %{"op" => "add_to_index", "activity" => activity.id}
-
- Config
- |> expect(:get, fn
- [Pleroma.Search, :module], nil ->
- Meilisearch
- end)
-
- assert_enqueued(worker: SearchIndexingWorker, args: args)
- assert :ok = perform_job(SearchIndexingWorker, args)
- end)
- end
-
test "deletes posts from index when deleted locally" do
user = insert(:user)
diff --git a/test/pleroma/search_test.exs b/test/pleroma/search_test.exs
new file mode 100644
index 000000000..d777bcda2
--- /dev/null
+++ b/test/pleroma/search_test.exs
@@ -0,0 +1,69 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.SearchTest do
+ use Pleroma.DataCase, async: true
+ use Oban.Testing, repo: Pleroma.Repo
+
+ import Pleroma.Factory
+
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Workers.SearchIndexingWorker
+
+ test "indexes posts that are public" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Well this is a story all about how my life got flipped turned upside down",
+ visibility: "public"
+ })
+
+ args = %{"op" => "add_to_index", "activity" => activity.id}
+
+ assert_enqueued(worker: SearchIndexingWorker, args: args)
+ end
+
+ test "doesn't index posts that are not public" do
+ user = insert(:user)
+
+ Enum.each(["private", "direct"], fn visibility ->
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "guys i just don't wanna leave the swamp",
+ visibility: visibility
+ })
+
+ args = %{"op" => "add_to_index", "activity" => activity.id}
+
+ refute_enqueued(worker: SearchIndexingWorker, args: args)
+ end)
+ end
+
+ test "Indexes appropriate activity types" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "I'm my own hype man",
+ visibility: "public"
+ })
+
+ args = %{"op" => "add_to_index", "activity" => activity.id}
+
+ assert_enqueued(worker: SearchIndexingWorker, args: args)
+
+ {:ok, fav_activity} = CommonAPI.favorite(activity.id, user)
+
+ args = %{"op" => "add_to_index", "activity" => fav_activity.id}
+
+ refute_enqueued(worker: SearchIndexingWorker, args: args)
+
+ {:ok, repeat_activity} = CommonAPI.repeat(activity.id, user)
+
+ args = %{"op" => "add_to_index", "activity" => repeat_activity.id}
+
+ refute_enqueued(worker: SearchIndexingWorker, args: args)
+ end
+end
diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs
index 572d7acc3..0c7c4c840 100644
--- a/test/pleroma/signature_test.exs
+++ b/test/pleroma/signature_test.exs
@@ -11,6 +11,7 @@ defmodule Pleroma.SignatureTest do
import Mock
alias Pleroma.Signature
+ alias Pleroma.StubbedHTTPSignaturesMock, as: HTTPSignaturesMock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -103,6 +104,18 @@ defmodule Pleroma.SignatureTest do
end
end
+ describe "validate_signature/1" do
+ test "treats HTTP signature errors as failed validation" do
+ conn = %Plug.Conn{method: "GET", request_path: "/inbox", req_headers: []}
+
+ Mox.expect(HTTPSignaturesMock, :validate_conn, fn _conn ->
+ {:error, :request_target_header}
+ end)
+
+ assert Signature.validate_signature(conn) == false
+ end
+ end
+
describe "key_id_to_actor_id/1" do
test "it properly deduces the actor id for misskey" do
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
diff --git a/test/pleroma/user/search_test.exs b/test/pleroma/user/search_test.exs
new file mode 100644
index 000000000..c1aca90bc
--- /dev/null
+++ b/test/pleroma/user/search_test.exs
@@ -0,0 +1,195 @@
+# Pleroma: A lightweight social networking server
+# Copyright © Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.User.SearchTest do
+ use Pleroma.DataCase, async: false
+
+ import Pleroma.Factory
+
+ alias Pleroma.Instances
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ describe "search/2 mention suggestions" do
+ test "prioritizes followed/follower users before others" do
+ user = insert(:user)
+
+ related =
+ insert(:user,
+ local: false,
+ nickname: "hj@real.example",
+ ap_id: "https://real.example/users/hj",
+ last_status_at: ~N[2020-01-01 00:00:00]
+ )
+
+ other = insert(:user, nickname: "hj", last_status_at: ~N[2020-01-02 00:00:00])
+
+ {:ok, _related, _user} = User.follow(related, user)
+
+ results = User.search("hj", for_user: user) |> Enum.map(& &1.id)
+
+ assert results == [related.id, other.id]
+ end
+
+ test "orders followed/follower users by most recent activity" do
+ user = insert(:user)
+
+ older =
+ insert(:user,
+ local: false,
+ nickname: "ali@remote.example",
+ ap_id: "https://remote.example/users/ali",
+ last_status_at: ~N[2020-01-01 00:00:00]
+ )
+
+ newer =
+ insert(:user,
+ local: false,
+ nickname: "alia@remote.example",
+ ap_id: "https://remote.example/users/alia",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ {:ok, _user, _older} = User.follow(user, older)
+ {:ok, _user, _newer} = User.follow(user, newer)
+
+ assert [newer.id, older.id] ==
+ User.search("ali", for_user: user)
+ |> Enum.map(& &1.id)
+ end
+
+ test "groups followed/follower users first and sorts them by recency" do
+ user = insert(:user)
+
+ following_newest =
+ insert(:user,
+ local: false,
+ nickname: "mentiontesta@related.example",
+ ap_id: "https://related.example/users/mentiontesta",
+ last_status_at: ~N[2020-01-03 00:00:00]
+ )
+
+ follower_middle =
+ insert(:user,
+ local: false,
+ nickname: "mentiontestb@related.example",
+ ap_id: "https://related.example/users/mentiontestb",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ mutual_oldest =
+ insert(:user,
+ local: false,
+ nickname: "mentiontestc@related.example",
+ ap_id: "https://related.example/users/mentiontestc",
+ last_status_at: ~N[2020-01-01 00:00:00]
+ )
+
+ unrelated_newer =
+ insert(:user,
+ local: false,
+ nickname: "mentiontestd@unrelated.example",
+ ap_id: "https://unrelated.example/users/mentiontestd",
+ last_status_at: ~N[2020-01-04 00:00:00]
+ )
+
+ {:ok, _user, _following_newest} = User.follow(user, following_newest)
+ {:ok, _follower_middle, _user} = User.follow(follower_middle, user)
+
+ {:ok, _user, _mutual_oldest} = User.follow(user, mutual_oldest)
+ {:ok, _mutual_oldest, _user} = User.follow(mutual_oldest, user)
+
+ results = User.search("mentiontest", for_user: user)
+
+ assert Enum.map(results, & &1.id) ==
+ [following_newest.id, follower_middle.id, mutual_oldest.id, unrelated_newer.id]
+ end
+
+ test "uses last_active_at when last_status_at is missing" do
+ user = insert(:user)
+
+ older =
+ insert(:user,
+ local: false,
+ nickname: "activefallbacka@remote.example",
+ ap_id: "https://remote.example/users/activefallbacka",
+ last_status_at: nil,
+ last_active_at: ~N[2020-01-01 00:00:00]
+ )
+
+ newer =
+ insert(:user,
+ local: false,
+ nickname: "activefallbackb@remote.example",
+ ap_id: "https://remote.example/users/activefallbackb",
+ last_status_at: nil,
+ last_active_at: ~N[2020-01-02 00:00:00]
+ )
+
+ {:ok, _user, _older} = User.follow(user, older)
+ {:ok, _user, _newer} = User.follow(user, newer)
+
+ assert [newer.id, older.id] ==
+ User.search("activefallback", for_user: user)
+ |> Enum.map(& &1.id)
+ end
+
+ test "does not return deactivated users even if related" do
+ user = insert(:user)
+
+ active =
+ insert(:user,
+ local: false,
+ nickname: "deactivatedtesta@remote.example",
+ ap_id: "https://remote.example/users/deactivatedtesta",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ deactivated =
+ insert(:user,
+ local: false,
+ nickname: "deactivatedtestb@remote.example",
+ ap_id: "https://remote.example/users/deactivatedtestb",
+ last_status_at: ~N[2020-01-03 00:00:00]
+ )
+
+ {:ok, _user, _active} = User.follow(user, active)
+ {:ok, _user, _deactivated} = User.follow(user, deactivated)
+ Repo.update!(Ecto.Changeset.change(deactivated, is_active: false))
+
+ results = User.search("deactivatedtest", for_user: user) |> Enum.map(& &1.id)
+
+ assert results == [active.id]
+ end
+
+ test "does not return users from unreachable instances" do
+ user = insert(:user)
+
+ {:ok, _instance} = Instances.set_unreachable("dead.example")
+
+ dead =
+ insert(:user,
+ local: false,
+ nickname: "ali@dead.example",
+ ap_id: "https://dead.example/users/ali",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ alive =
+ insert(:user,
+ local: false,
+ nickname: "ali@alive.example",
+ ap_id: "https://alive.example/users/ali",
+ last_status_at: ~N[2020-01-02 00:00:00]
+ )
+
+ {:ok, _user, _alive} = User.follow(user, alive)
+ {:ok, _user, _dead} = User.follow(user, dead)
+
+ results = User.search("ali", for_user: user) |> Enum.map(& &1.id)
+
+ assert results == [alive.id]
+ end
+ end
+end
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index b2533e9f1..de735cdb7 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -876,17 +876,17 @@ defmodule Pleroma.UserTest do
describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
- test "for mastodon" do
- ap_id = "a@mastodon.example"
- {:ok, fetched_user} = User.get_or_fetch(ap_id)
+ test "fetches a mastodon split-domain nickname" do
+ nickname = "a@mastodon.example"
+ {:ok, fetched_user} = User.get_or_fetch(nickname)
assert fetched_user.ap_id == "https://sub.mastodon.example/users/a"
assert fetched_user.nickname == "a@mastodon.example"
end
- test "for pleroma" do
- ap_id = "a@pleroma.example"
- {:ok, fetched_user} = User.get_or_fetch(ap_id)
+ test "fetches a pleroma split-domain nickname" do
+ nickname = "a@pleroma.example"
+ {:ok, fetched_user} = User.get_or_fetch(nickname)
assert fetched_user.ap_id == "https://sub.pleroma.example/users/a"
assert fetched_user.nickname == "a@pleroma.example"
@@ -936,6 +936,89 @@ defmodule Pleroma.UserTest do
assert fetched_user == "not found nonexistent"
end
+ test "does not rename an existing remote actor from rogue WebFinger data" do
+ clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
+
+ actor_id = "https://legit-actor.example/users/alice"
+
+ Tesla.Mock.mock(fn
+ %{url: "https://evil-webfinger.example/.well-known/host-meta"} ->
+ {:ok, %Tesla.Env{status: 404}}
+
+ %{
+ url:
+ "https://evil-webfinger.example/.well-known/webfinger?resource=acct:claimed@evil-webfinger.example"
+ } ->
+ Tesla.Mock.json(%{
+ "subject" => "acct:claimed@evil-webfinger.example",
+ "links" => [
+ %{
+ "rel" => "self",
+ "type" => "application/activity+json",
+ "href" => actor_id
+ }
+ ]
+ })
+
+ %{url: ^actor_id} ->
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ headers: [{"content-type", "application/activity+json"}],
+ body:
+ Jason.encode!(%{
+ "id" => actor_id,
+ "type" => "Person",
+ "preferredUsername" => "alice",
+ "name" => "Alice",
+ "summary" => "",
+ "inbox" => "https://legit-actor.example/users/alice/inbox",
+ "outbox" => "https://legit-actor.example/users/alice/outbox",
+ "followers" => "https://legit-actor.example/users/alice/followers",
+ "following" => "https://legit-actor.example/users/alice/following"
+ })
+ }}
+
+ %{url: "https://legit-actor.example/.well-known/host-meta"} ->
+ {:ok, %Tesla.Env{status: 404}}
+
+ %{
+ url:
+ "https://legit-actor.example/.well-known/webfinger?resource=acct:alice@legit-actor.example"
+ } ->
+ Tesla.Mock.json(%{
+ "subject" => "acct:alice@legit-actor.example",
+ "links" => [
+ %{
+ "rel" => "self",
+ "type" => "application/activity+json",
+ "href" => actor_id
+ }
+ ]
+ })
+ end)
+
+ assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
+ ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
+
+ refute User.get_by_ap_id(actor_id)
+ refute User.get_by_nickname("claimed@evil-webfinger.example")
+
+ orig_user =
+ insert(:user,
+ local: false,
+ nickname: "alice@legit-actor.example",
+ ap_id: actor_id
+ )
+
+ assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
+ ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
+
+ assert {:error, _} = User.get_or_fetch_by_nickname("claimed@evil-webfinger.example")
+ assert User.get_by_id(orig_user.id).nickname == "alice@legit-actor.example"
+ refute User.get_by_nickname("claimed@evil-webfinger.example")
+ end
+
test "updates an existing user, if stale" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
index d5947186f..3988c3912 100644
--- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Workers.ReceiverWorker
+ alias Pleroma.Workers.SignatureRetryWorker
import Pleroma.Factory
@@ -36,6 +37,36 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
setup do: clear_config([:instance, :federating], true)
+ defp assign_valid_signature_for_actor(conn, %User{ap_id: actor_id}) do
+ assign_valid_signature_for_actor(conn, actor_id)
+ end
+
+ defp assign_valid_signature_for_actor(conn, actor) do
+ actor_id = Utils.get_ap_id(actor)
+
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("signature", "keyId=\"#{actor_id}#main-key\"")
+ end
+
+ defp expect_signature_retry_from(%User{} = signer) do
+ signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured")
+
+ Tesla.Mock.mock(fn
+ %{url: url} when url == signer.ap_id ->
+ %Tesla.Env{
+ status: 200,
+ body: Jason.encode!(signer_json),
+ headers: HttpRequestMock.activitypub_object_headers()
+ }
+
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
+
+ Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
+ end
+
describe "/relay" do
setup do: clear_config([:instance, :allow_relay])
@@ -688,7 +719,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
@@ -716,7 +747,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
@@ -726,6 +757,199 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert Activity.get_by_ap_id(data["id"])
end
+ test "does not create a forged post after failed signature retry", %{conn: conn} do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+ object_id = "https://two.com/objects/inbox-forged-note"
+
+ data = %{
+ "type" => "Create",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/inbox-forged-create",
+ "context" => "https://two.com/contexts/inbox-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => object_id,
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "context" => "https://two.com/contexts/inbox-forged-create",
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ expect_signature_retry_from(alice)
+
+ conn =
+ conn
+ |> assign(:valid_signature, false)
+ |> put_req_header("content-type", "application/activity+json")
+ |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
+ |> post("/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+
+ assert [{:cancel, :actor_signature_mismatch}] =
+ ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
+
+ refute Activity.get_by_ap_id(data["id"])
+ refute Object.get_by_ap_id(object_id)
+ end
+
+ test "does not create a forged like after failed signature retry", %{conn: conn} do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+ note = insert(:note)
+
+ data = %{
+ "type" => "Like",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/inbox-forged-like",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data["id"]
+ }
+
+ expect_signature_retry_from(alice)
+
+ conn =
+ conn
+ |> assign(:valid_signature, false)
+ |> put_req_header("content-type", "application/activity+json")
+ |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
+ |> post("/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+
+ assert [{:cancel, :actor_signature_mismatch}] =
+ ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
+
+ refute Activity.get_by_ap_id(data["id"])
+ end
+
+ test "does not delete an object after failed signature retry", %{conn: conn} do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+ note = insert(:note)
+ object_id = note.data["id"]
+
+ data = %{
+ "type" => "Delete",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/inbox-forged-delete",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => object_id
+ }
+
+ expect_signature_retry_from(alice)
+
+ conn =
+ conn
+ |> assign(:valid_signature, false)
+ |> put_req_header("content-type", "application/activity+json")
+ |> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
+ |> post("/inbox", data)
+
+ assert "ok" == json_response(conn, 200)
+
+ assert [{:cancel, :actor_signature_mismatch}] =
+ ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
+
+ refute Activity.get_by_ap_id(data["id"])
+ assert %Object{data: %{"type" => "Note"}} = Object.get_by_ap_id(object_id)
+ end
+
+ test "does not create a forged post signed by a different actor", %{conn: conn} do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+ object_id = "https://two.com/objects/inbox-signed-forged-note"
+
+ data = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "type" => "Create",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/inbox-signed-forged-create",
+ "context" => "https://two.com/contexts/inbox-signed-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => object_id,
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "context" => "https://two.com/contexts/inbox-signed-forged-create",
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ expect_signature_retry_from(alice)
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/activity+json")
+ |> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT")
+ |> put_req_header("digest", "SHA-256=fake-digest")
+ |> put_req_header(
+ "signature",
+ "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
+ )
+ |> post("/inbox", data)
+
+ assert conn.assigns.valid_signature == false
+ assert "ok" == json_response(conn, 200)
+
+ assert [{:cancel, :actor_signature_mismatch}] =
+ ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
+
+ refute Activity.get_by_ap_id(data["id"])
+ refute Object.get_by_ap_id(object_id)
+ end
+
+ test "does not create a forged like signed by a different actor", %{conn: conn} do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+ note = insert(:note)
+
+ data = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "type" => "Like",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/inbox-signed-forged-like",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data["id"]
+ }
+
+ expect_signature_retry_from(alice)
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/activity+json")
+ |> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT")
+ |> put_req_header("digest", "SHA-256=fake-digest")
+ |> put_req_header(
+ "signature",
+ "keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
+ )
+ |> post("/inbox", data)
+
+ assert conn.assigns.valid_signature == false
+ assert "ok" == json_response(conn, 200)
+
+ assert [{:cancel, :actor_signature_mismatch}] =
+ ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
+
+ refute Activity.get_by_ap_id(data["id"])
+ end
+
test "accept follow activity", %{conn: conn} do
clear_config([:instance, :federating], true)
relay = Relay.get_actor()
@@ -742,7 +966,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" ==
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(followed_relay)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", accept)
|> json_response(200)
@@ -822,16 +1046,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
test "Unknown activity types are discarded", %{conn: conn} do
unknown_types = ["Poke", "Read", "Dazzle"]
+ actor =
+ insert(:user, local: false, ap_id: "https://unknown.mastodon.instance/users/somebody")
+
Enum.each(unknown_types, fn bad_type ->
params =
%{
"type" => bad_type,
- "actor" => "https://unknown.mastodon.instance/users/somebody"
+ "actor" => actor.ap_id
}
|> Jason.encode!()
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(actor)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", params)
|> json_response(400)
@@ -900,7 +1127,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" ==
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
@@ -921,7 +1148,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" ==
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
@@ -990,7 +1217,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" ==
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
@@ -1009,7 +1236,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" ==
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
@@ -1040,7 +1267,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@@ -1061,7 +1288,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@@ -1082,7 +1309,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@@ -1106,7 +1333,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@@ -1133,7 +1360,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@@ -1163,7 +1390,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
@@ -1228,7 +1455,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
}
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
|> json_response(200)
@@ -1318,7 +1545,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
}
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200)
@@ -1372,7 +1599,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
}
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200)
@@ -1405,7 +1632,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@@ -1428,7 +1655,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@@ -1451,7 +1678,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
- |> assign(:valid_signature, true)
+ |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
@@ -2571,6 +2798,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
setup do: clear_config([:media_proxy])
setup do: clear_config([Pleroma.Upload])
+ # majic's libmagic port is unavailable on local Darwin runs; Linux CI still runs this test.
+ @tag :skip_darwin
test "POST /api/ap/upload_media", %{conn: conn} do
user = insert(:user)
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index c32811c5b..bf9c70fb6 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -149,6 +149,171 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
+ test "a Misskey MFM note is rendered from source content" do
+ user = insert(:user, ap_id: "https://misskey.example/users/alice")
+
+ note = %{
+ "id" => "https://misskey.example/notes/1",
+ "type" => "Note",
+ "actor" => user.ap_id,
+ "attributedTo" => user.ap_id,
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "content" => "original content",
+ "context" => Utils.generate_context_id(),
+ "source" => %{
+ "content" => "$[spin.speed=1s mfm goes here] ",
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+ }
+
+ %{valid?: true, changes: %{content: content, source: source}} =
+ ArticleNotePageValidator.cast_and_validate(note)
+
+ assert source["mediaType"] == "text/x.misskeymarkdown"
+ assert content =~ ~s(class="mfm-spin")
+ assert content =~ ~s(data-mfm-speed="1s")
+ assert content =~ "mfm goes here"
+ refute content =~ "original content"
+ refute content =~ "",
+ "htmlMfm" => true,
+ "context" => Utils.generate_context_id(),
+ "source" => %{
+ "content" => String.duplicate("x", 5_001),
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+ }
+
+ %{valid?: true, changes: %{content: content, htmlMfm: true, source: source}} =
+ ArticleNotePageValidator.cast_and_validate(note)
+
+ assert content == "already renderedalert('xss')"
+ refute Map.has_key?(source, "content")
+ end
+
test "a Note with validated likes collection validates" do
insert(:user, ap_id: "https://pol.social/users/mkljczk")
diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs
index 347c5b578..9dec315b3 100644
--- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
assert {:ok, _update, []} = ObjectValidator.validate(valid_update, [])
end
- test "returns an error if the object can't be updated by the actor", %{
+ test "returns an error if the object can't be updated by the actor (different domain)", %{
valid_update: valid_update
} do
other_user = insert(:user, local: false)
@@ -41,27 +41,72 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
assert {:error, _cng} = ObjectValidator.validate(update, [])
end
- test "validates as long as the object is same-origin with the actor", %{
+ test "returns an error if the object can't be updated by the actor (same domain)", %{
+ user: user,
valid_update: valid_update
} do
- other_user = insert(:user)
+ user_ap_id = user.ap_id
+ user_domain = URI.parse(user_ap_id).host
+ other_user = insert(:user, local: false, domain: user_domain)
update =
valid_update
|> Map.put("actor", other_user.ap_id)
- assert {:ok, _update, []} = ObjectValidator.validate(update, [])
+ assert {:error, _cng} = ObjectValidator.validate(update, [])
end
- test "validates if the object is not of an Actor type" do
- note = insert(:note)
+ test "validates if the object is not of an Actor type", %{user: user} do
+ note = insert(:note, user: user)
updated_note = note.data |> Map.put("content", "edited content")
- other_user = insert(:user)
- {:ok, update, _} = Builder.update(other_user, updated_note)
+ {:ok, update, _} = Builder.update(user, updated_note)
assert {:ok, _update, _} = ObjectValidator.validate(update, [])
end
+
+ test "returns an error if the remote update target is unknown" do
+ remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice")
+
+ update = %{
+ "type" => "Update",
+ "actor" => remote_user.ap_id,
+ "id" => "https://example.com/activities/update-unknown-object",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://example.com/objects/unknown",
+ "actor" => remote_user.ap_id,
+ "content" => "edited content",
+ "published" => "2024-07-25T13:33:31Z",
+ "updated" => "2024-07-25T13:34:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false)
+ refute cng.valid?
+ assert Keyword.has_key?(cng.errors, :object)
+ end
+
+ test "returns an error if the remote update target IRI is unknown" do
+ remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice")
+
+ update = %{
+ "type" => "Update",
+ "actor" => remote_user.ap_id,
+ "id" => "https://example.com/activities/update-unknown-object-iri",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => "https://example.com/objects/unknown-iri"
+ }
+
+ assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false)
+ refute cng.valid?
+ assert Keyword.has_key?(cng.errors, :object)
+ end
end
describe "update note" do
diff --git a/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs
index d31070546..2a4e78cf0 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs
@@ -170,4 +170,23 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do
assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
end
+
+ test "it displays voters count for a poll" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "???",
+ poll: %{expires_in: 10, options: ["yes", "no"]}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+ {:ok, _, _} = CommonAPI.vote(object, other_user, [1])
+
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
+
+ refute Map.has_key?(modified["object"], "voters")
+ assert modified["object"]["votersCount"] == 1
+ end
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index c1e01557d..bedb466a4 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -86,6 +86,43 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert activity.data["cc"] == [user.ap_id]
end
+ test "it rejects Flag activities when both reporter and reported account are remote" do
+ reporter = insert(:user, local: false, domain: "mastodon.cat")
+ reported = insert(:user, local: false, domain: "nicecrew.digital")
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "actor" => reporter.ap_id,
+ "content" => "blocked AND reported!!!",
+ "object" => [reported.ap_id, "https://nicecrew.digital/objects/report-status"],
+ "type" => "Flag"
+ }
+
+ assert {:reject, reason} = Transmogrifier.handle_incoming(message)
+ assert reason =~ "third-party report"
+ refute "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
+ end
+
+ test "it accepts Flag activities with just actor id as object" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ message = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "cc" => [user.ap_id],
+ "object" => user.ap_id,
+ "type" => "Flag",
+ "content" => "blocked AND reported!!!",
+ "actor" => other_user.ap_id
+ }
+
+ assert {:ok, activity} = Transmogrifier.handle_incoming(message)
+
+ assert activity.data["content"] == "blocked AND reported!!!"
+ assert activity.data["actor"] == other_user.ap_id
+ assert activity.data["cc"] == [user.ap_id]
+ end
+
test "it accepts Move activities" do
old_user = insert(:user)
new_user = insert(:user)
diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs
index 3b77f0867..93234a015 100644
--- a/test/pleroma/web/activity_pub/utils_test.exs
+++ b/test/pleroma/web/activity_pub/utils_test.exs
@@ -180,7 +180,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
- "@language" => "und"
+ "@language" => "und",
+ "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
@@ -192,7 +193,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
- "@language" => "pl"
+ "@language" => "pl",
+ "htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 017fac696..5d0ad4572 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -709,6 +709,47 @@ defmodule Pleroma.Web.CommonAPITest do
assert object.data["source"]["content"] == post
end
+ test "it renders MFM posts and marks their ActivityPub representation" do
+ user = insert(:user)
+
+ post = "
$[spin.speed=1s 13:37]
"
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: post,
+ content_type: "text/x.misskeymarkdown"
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ assert object.data["htmlMfm"] == true
+
+ assert object.data["source"] == %{
+ "content" => post,
+ "mediaType" => "text/x.misskeymarkdown"
+ }
+
+ assert object.data["content"] =~ ~s(class="mfm-spin")
+ assert object.data["content"] =~ ~s(data-mfm-speed="1s")
+ assert object.data["content"] =~ "13:37"
+ refute object.data["content"] =~ "scrub-this"
+ end
+
+ test "it falls back safely for malformed MFM" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "$[spin malformed",
+ content_type: "text/x.misskeymarkdown"
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ refute object.data["content"] =~ ~s(class="mfm-spin")
+ assert object.data["content"] =~ "malformed"
+ end
+
test "it does not allow replies to direct messages that are not direct messages themselves" do
user = insert(:user)
diff --git a/test/pleroma/web/fallback_test.exs b/test/pleroma/web/fallback_test.exs
index 6d0ba3d2a..bc3052959 100644
--- a/test/pleroma/web/fallback_test.exs
+++ b/test/pleroma/web/fallback_test.exs
@@ -79,6 +79,12 @@ defmodule Pleroma.Web.FallbackTest do
test "GET /phoenix/live_dashboard -> /pleroma/live_dashboard", %{conn: conn} do
assert redirected_to(get(conn, "/phoenix/live_dashboard")) =~ "/pleroma/live_dashboard"
+ assert redirected_to(get(conn, "/phoenix/live_dashboard/")) =~ "/pleroma/live_dashboard/"
+ end
+
+ test "GET /phoenix/live_dashboard/* -> /pleroma/live_dashboard/*", %{conn: conn} do
+ assert redirected_to(get(conn, "/phoenix/live_dashboard/ecto_stats?nav=diagnose")) =~
+ "/pleroma/live_dashboard/ecto_stats?nav=diagnose"
end
test "OPTIONS /*path", %{conn: conn} do
diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
index 67f292506..01e195842 100644
--- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
@@ -1815,7 +1815,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
test "returns lists to which the account belongs" do
%{user: user, conn: conn} = oauth_access(["read:lists"])
other_user = insert(:user)
- assert {:ok, %Pleroma.List{id: _list_id} = list} = Pleroma.List.create(%{title: "Test List"}, user)
+
+ assert {:ok, %Pleroma.List{id: _list_id} = list} =
+ Pleroma.List.create(%{title: "Test List"}, user)
+
{:ok, %{following: _following}} = Pleroma.List.follow(list, other_user)
assert [%{"id" => _list_id, "title" => "Test List"}] =
diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
index 6de001421..6cb5934de 100644
--- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
@@ -167,10 +167,192 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
} = PollView.render("show.json", %{object: object})
end
- test "that poll is non anonymous" do
+ test "displays correct voters count" do
+ object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true)
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 14
+ end
+
+ test "detects that poll is non anonymous" do
object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true)
result = PollView.render("show.json", %{object: object})
assert result[:pleroma][:non_anonymous] == true
end
+
+ test "prefers votersCount over voters list when both are present" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0])
+
+ assert object.data["votersCount"] == 1
+ assert length(object.data["voters"]) == 1
+
+ object = %{
+ object
+ | data: Map.put(object.data, "votersCount", 42)
+ }
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 42
+ end
+
+ test "falls back to voters list when votersCount is absent" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0])
+
+ assert length(object.data["voters"]) == 1
+
+ data = Map.delete(object.data, "votersCount")
+ object = %{object | data: data}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 1
+ end
+
+ test "returns 0 when both votersCount and voters are absent" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ data =
+ object.data
+ |> Map.delete("votersCount")
+ |> Map.delete("voters")
+
+ object = %{object | data: data}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 0
+ end
+
+ test "returns 0 when voters list is empty" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Which flavor?",
+ poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ data =
+ object.data
+ |> Map.delete("votersCount")
+ |> Map.put("voters", [])
+
+ object = %{object | data: data}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 0
+ end
+
+ test "does not inflate votersCount when same voter picks multiple options" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick several",
+ poll: %{options: ["a", "b", "c"], expires_in: 20, multiple: true}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0, 2])
+
+ assert object.data["votersCount"] == 1
+ assert length(object.data["voters"]) == 1
+ end
+
+ test "preserves votersCount from remote source when existing voter picks another option" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick several",
+ poll: %{options: ["a", "b"], expires_in: 20, multiple: true}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0, 1])
+
+ object = %{object | data: Map.put(object.data, "votersCount", 14)}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 14
+ end
+
+ test "returns 0 when votersCount is explicitly 0" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick one",
+ poll: %{options: ["a", "b"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ object = %{object | data: Map.put(object.data, "votersCount", 0)}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == 0
+ end
+
+ test "falls back to voters list when votersCount is nil" do
+ user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "Pick one",
+ poll: %{options: ["a", "b"], expires_in: 20}
+ })
+
+ object = Object.normalize(activity, fetch: false)
+
+ voter = insert(:user)
+ {:ok, _, object} = CommonAPI.vote(object, voter, [0])
+
+ object = %{object | data: Map.put(object.data, "votersCount", nil)}
+
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:voters_count] == length(object.data["voters"])
+ end
end
diff --git a/test/pleroma/web/plugs/favicon_plug_test.exs b/test/pleroma/web/plugs/favicon_plug_test.exs
new file mode 100644
index 000000000..520501250
--- /dev/null
+++ b/test/pleroma/web/plugs/favicon_plug_test.exs
@@ -0,0 +1,63 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2026 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.FaviconPlugTest do
+ use Pleroma.Web.ConnCase
+
+ @dir "test/tmp/favicon_static"
+
+ setup do
+ Pleroma.Backports.mkdir_p!(@dir)
+
+ on_exit(fn -> File.rm_rf!(@dir) end)
+ end
+
+ describe "default favicon" do
+ test "returns favicon", %{conn: conn} do
+ conn = get(conn, "/favicon.png")
+ body_size = byte_size(conn.resp_body)
+
+ assert conn.status == 200
+ assert body_size == 1583
+ assert response_content_type(conn, :png)
+ end
+
+ test "returns correct cache-control", %{conn: conn} do
+ conn = get(conn, "/favicon.png")
+ cache = get_resp_header(conn, "cache-control")
+
+ assert conn.status == 200
+ assert cache == ["public, max=age=86400, immutable"]
+ end
+ end
+
+ describe "custom favicon" do
+ setup do
+ favicon_path = Path.join(@dir, "favicon.png")
+ donor_image = "test/fixtures/image.png"
+
+ File.cp!(donor_image, favicon_path)
+ clear_config([:instance, :static_dir], @dir)
+
+ on_exit(fn -> File.rm!(favicon_path) end)
+ end
+
+ test "returns favicon", %{conn: conn} do
+ conn = get(conn, "/favicon.png")
+ body_size = byte_size(conn.resp_body)
+
+ assert conn.status == 200
+ assert body_size == 104_426
+ assert response_content_type(conn, :png)
+ end
+
+ test "returns correct cache-control", %{conn: conn} do
+ conn = get(conn, "/favicon.png")
+ cache = get_resp_header(conn, "cache-control")
+
+ assert conn.status == 200
+ assert cache == ["public, max=age=86400, immutable"]
+ end
+ end
+end
diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs
index e1e331c06..b7c06eacd 100644
--- a/test/pleroma/web/plugs/frontend_static_plug_test.exs
+++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs
@@ -105,6 +105,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
"nodeinfo",
"manifest.json",
"auth",
+ "embed",
"proxy",
"test",
"user_exists",
diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs
index b5a5a3334..017c49d1e 100644
--- a/test/pleroma/web/plugs/instance_static_test.exs
+++ b/test/pleroma/web/plugs/instance_static_test.exs
@@ -137,4 +137,47 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do
# It should be preserved because "image" is in the allowed_mime_types list
assert content_type == "image/jpeg"
end
+
+ describe "404s for missing files in static-only paths" do
+ test "returns 404 for non-existent static-only JSON files" do
+ conn = get(build_conn(), "/static/non-existent.json")
+
+ assert conn.status == 404
+ assert ["application/json"] = get_resp_header(conn, "content-type")
+ assert Jason.decode!(conn.resp_body) == %{"error" => "not found"}
+ end
+
+ test "returns 404 for non-existent static-only non-JSON files" do
+ conn = get(build_conn(), "/static/non-existent.txt")
+
+ assert conn.status == 404
+ assert conn.resp_body == "Not found"
+ assert ["text/plain"] = get_resp_header(conn, "content-type")
+ end
+
+ test "returns 404 for non-existent .css files" do
+ conn = get(build_conn(), "/static/non-existent.css")
+
+ assert conn.status == 404
+ assert conn.resp_body == "Not found"
+ # Verifies that we forced text/plain for the error body, even though the path was .css
+ assert ["text/plain"] = get_resp_header(conn, "content-type")
+ end
+
+ test "returns 404 for non-existent files without an extension" do
+ conn = get(build_conn(), "/static/non-existent")
+
+ assert conn.status == 404
+ assert conn.resp_body == "Not found"
+ assert ["text/plain"] = get_resp_header(conn, "content-type")
+ end
+
+ test "returns 200 (falls through to SPA) for non-static-only paths" do
+ # /some-route is NOT in static_only_files, so it should still fall through to the SPA.
+ conn = get(build_conn(), "/some-route")
+
+ assert conn.status == 200
+ assert ["text/html; charset=utf-8"] = get_resp_header(conn, "content-type")
+ end
+ end
end
diff --git a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs
index 33eff1bc5..df713762c 100644
--- a/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs
+++ b/test/pleroma/web/plugs/mapped_signature_to_identity_plug_test.exs
@@ -47,13 +47,27 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
assert %{valid_signature: false} == conn.assigns
end
- @tag skip: "known breakage; the testsuite presently depends on it"
test "it considers a mapped identity to be invalid when the identity cannot be found" do
+ actor = "http://niu.moe/users/rye"
+
conn =
- build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
- |> set_signature("http://niu.moe/users/rye")
+ build_conn(:post, "/doesntmattter", %{"actor" => actor})
+ |> set_signature(actor)
|> MappedSignatureToIdentityPlug.call(%{})
- assert %{valid_signature: false} == conn.assigns
+ assert conn.assigns.valid_signature == false
+ refute Map.has_key?(conn.assigns, :user)
+ end
+
+ test "it considers a mapped identity to be invalid when embedded actor identity cannot be found" do
+ actor = "http://niu.moe/users/rye"
+
+ conn =
+ build_conn(:post, "/doesntmattter", %{"actor" => %{"id" => actor}})
+ |> set_signature(actor)
+ |> MappedSignatureToIdentityPlug.call(%{})
+
+ assert conn.assigns.valid_signature == false
+ refute Map.has_key?(conn.assigns, :user)
end
end
diff --git a/test/pleroma/web/plugs/rate_limiter_test.exs b/test/pleroma/web/plugs/rate_limiter_test.exs
index 19cee8aee..10c93fa73 100644
--- a/test/pleroma/web/plugs/rate_limiter_test.exs
+++ b/test/pleroma/web/plugs/rate_limiter_test.exs
@@ -268,6 +268,23 @@ defmodule Pleroma.Web.Plugs.RateLimiterTest do
refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts)
end
+ test "doesn't crash if rate limit scale is invalid (e.g. broken DB config)" do
+ limiter_name = :test_invalid_rate_limit_config
+
+ clear_config([:rate_limit, limiter_name], [{"", 0}, {"", ""}])
+ clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
+
+ opts = RateLimiter.init(name: limiter_name)
+
+ conn = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 1}}
+
+ conn_limited = RateLimiter.call(conn, opts)
+
+ refute conn_limited.status == Conn.Status.code(:too_many_requests)
+ refute conn_limited.resp_body
+ refute conn_limited.halted
+ end
+
def expire_ttl(%{remote_ip: remote_ip} = _conn, bucket_name_root) do
bucket_name = "anon:#{bucket_name_root}" |> String.to_atom()
key_name = "ip::#{remote_ip |> Tuple.to_list() |> Enum.join(".")}"
diff --git a/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs
index 9df86c0a2..9236d1bf8 100644
--- a/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs
+++ b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs
@@ -83,7 +83,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do
response =
conn
- |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
+ |> get(
+ remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})
+ )
|> html_response(200)
assert response =~ "Log in to follow"
@@ -114,7 +116,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do
response =
conn
|> assign(:user, user)
- |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
+ |> get(
+ remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})
+ )
|> html_response(200)
assert response =~ "Remote follow"
@@ -157,7 +161,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do
conn
|> assign(:user, user)
|> assign(:token, read_token)
- |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
+ |> post(remote_interaction_path(conn, :do_follow), %{
+ "user" => %{"id" => user2.id}
+ })
|> response(200)
assert response =~ "Error following account"
@@ -496,7 +502,9 @@ defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do
)
assert redirected_to(conn) ==
- remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})
+ remote_interaction_path(conn, :follow, %{
+ acct: "https://mastodon.social/users/emelie"
+ })
end
end
diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs
index 12abc1a27..ea05f38f1 100644
--- a/test/pleroma/workers/receiver_worker_test.exs
+++ b/test/pleroma/workers/receiver_worker_test.exs
@@ -11,9 +11,27 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
alias Pleroma.User
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.Federator
alias Pleroma.Workers.ReceiverWorker
+ defp signature_headers_for(%User{} = signer) do
+ [
+ {"host", "local.test"},
+ {"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
+ {"digest", "SHA-256=fake-digest"},
+ {"content-type", "application/activity+json"},
+ {
+ "signature",
+ "keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
+ }
+ ]
+ end
+
+ defp perform_incoming(params) do
+ ReceiverWorker.perform(%Oban.Job{
+ args: %{"op" => "incoming_ap_doc", "params" => params}
+ })
+ end
+
test "it does not retry MRF reject" do
params = insert(:note).data
@@ -81,16 +99,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/bart")
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:cancel, {:error, :forbidden}} = ReceiverWorker.perform(oban_job)
+ assert {:cancel, {:error, :forbidden}} = perform_incoming(params)
end
test "when request returns a 404" do
@@ -98,16 +107,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/troymcclure")
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job)
+ assert {:cancel, {:error, :not_found}} = perform_incoming(params)
end
test "when request returns a 410" do
@@ -115,16 +115,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/hankscorpio")
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job)
+ assert {:cancel, {:error, :not_found}} = perform_incoming(params)
end
test "when user account is disabled" do
@@ -138,86 +129,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
{:ok, %User{}} = User.set_activation(user, false)
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:cancel, {:user_active, false}} = ReceiverWorker.perform(oban_job)
+ assert {:cancel, {:user_active, false}} = perform_incoming(params)
end
end
- test "it can validate the signature" do
- Tesla.Mock.mock(fn
- %{url: "https://phpc.social/users/denniskoch"} ->
- %Tesla.Env{
- status: 200,
- body: File.read!("test/fixtures/denniskoch.json"),
- headers: [{"content-type", "application/activity+json"}]
- }
-
- %{url: "https://phpc.social/users/denniskoch/collections/featured"} ->
- %Tesla.Env{
- status: 200,
- headers: [{"content-type", "application/activity+json"}],
- body:
- File.read!("test/fixtures/users_mock/masto_featured.json")
- |> String.replace("{{domain}}", "phpc.social")
- |> String.replace("{{nickname}}", "denniskoch")
- }
- end)
-
- params =
- File.read!("test/fixtures/receiver_worker_signature_activity.json") |> Jason.decode!()
-
- req_headers = [
- ["accept-encoding", "gzip"],
- ["content-length", "5184"],
- ["content-type", "application/activity+json"],
- ["date", "Thu, 25 Jul 2024 13:33:31 GMT"],
- ["digest", "SHA-256=ouge/6HP2/QryG6F3JNtZ6vzs/hSwMk67xdxe87eH7A="],
- ["host", "bikeshed.party"],
- [
- "signature",
- "keyId=\"https://mastodon.social/users/bastianallgeier#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"ymE3vn5Iw50N6ukSp8oIuXJB5SBjGAGjBasdTDvn+ahZIzq2SIJfmVCsIIzyqIROnhWyQoTbavTclVojEqdaeOx+Ejz2wBnRBmhz5oemJLk4RnnCH0lwMWyzeY98YAvxi9Rq57Gojuv/1lBqyGa+rDzynyJpAMyFk17XIZpjMKuTNMCbjMDy76ILHqArykAIL/v1zxkgwxY/+ELzxqMpNqtZ+kQ29znNMUBB3eVZ/mNAHAz6o33Y9VKxM2jw+08vtuIZOusXyiHbRiaj2g5HtN2WBUw1MzzfRfHF2/yy7rcipobeoyk5RvP5SyHV3WrIeZ3iyoNfmv33y8fxllF0EA==\""
- ],
- [
- "user-agent",
- "http.rb/5.2.0 (Mastodon/4.3.0-nightly.2024-07-25; +https://mastodon.social/)"
- ]
- ]
-
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: req_headers,
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:ok, %Pleroma.Activity{}} = ReceiverWorker.perform(oban_job)
- end
-
test "cancels due to origin containment" do
params =
insert(:note_activity).data
|> Map.put("id", "https://notorigindomain.com/activity")
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
-
- assert {:cancel, :origin_containment_failed} = ReceiverWorker.perform(oban_job)
+ assert {:cancel, :origin_containment_failed} = perform_incoming(params)
end
test "canceled due to deleted object" do
@@ -233,16 +154,114 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
}
end)
- {:ok, oban_job} =
- Federator.incoming_ap_doc(%{
- method: "POST",
- req_headers: [],
- request_path: "/inbox",
- params: params,
- query_string: ""
- })
+ assert {:cancel, _} = perform_incoming(params)
+ end
- assert {:cancel, _} = ReceiverWorker.perform(oban_job)
+ test "delegates legacy failed-signature metadata jobs instead of processing them as trusted" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+ object_id = "https://two.com/objects/legacy-forged-note"
+
+ create = %{
+ "type" => "Create",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/legacy-forged-create",
+ "context" => "https://two.com/contexts/legacy-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => object_id,
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "context" => "https://two.com/contexts/legacy-forged-create",
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ assert {:cancel, :actor_signature_mismatch} =
+ ReceiverWorker.perform(%Oban.Job{
+ args: %{
+ "op" => "incoming_ap_doc",
+ "method" => "POST",
+ "params" => create,
+ "req_headers" => signature_headers_for(alice),
+ "request_path" => "/inbox",
+ "query_string" => ""
+ }
+ })
+
+ refute Pleroma.Activity.get_by_ap_id(create["id"])
+ refute Pleroma.Object.get_by_ap_id(object_id)
+ end
+
+ test "fails closed for the old persisted failed-signature job shape" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+ object_id = "https://two.com/objects/old-shape-forged-note"
+
+ create = %{
+ "type" => "Create",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/old-shape-forged-create",
+ "context" => "https://two.com/contexts/old-shape-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => object_id,
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "context" => "https://two.com/contexts/old-shape-forged-create",
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ assert {:cancel, :missing_signature_retry_metadata} =
+ ReceiverWorker.perform(%Oban.Job{
+ args: %{
+ "op" => "incoming_ap_doc",
+ "params" => create,
+ "req_headers" => signature_headers_for(alice),
+ "timeout" => 20_000
+ }
+ })
+
+ refute Pleroma.Activity.get_by_ap_id(create["id"])
+ refute Pleroma.Object.get_by_ap_id(object_id)
+ end
+
+ test "fails closed for legacy retry jobs missing one metadata field" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ params = insert(:note_activity).data
+
+ assert {:cancel, :missing_signature_retry_metadata} =
+ ReceiverWorker.perform(%Oban.Job{
+ args: %{
+ "op" => "incoming_ap_doc",
+ "method" => "POST",
+ "params" => params,
+ "req_headers" => signature_headers_for(alice),
+ "request_path" => "/inbox"
+ }
+ })
+ end
+
+ test "fails closed for malformed legacy metadata jobs without params" do
+ assert {:cancel, :missing_signature_retry_metadata} =
+ ReceiverWorker.perform(%Oban.Job{
+ args: %{
+ "op" => "incoming_ap_doc",
+ "req_headers" => [],
+ "timeout" => 20_000
+ }
+ })
end
describe "Server reachability:" do
diff --git a/test/pleroma/workers/signature_retry_worker_test.exs b/test/pleroma/workers/signature_retry_worker_test.exs
new file mode 100644
index 000000000..94dd5f6c1
--- /dev/null
+++ b/test/pleroma/workers/signature_retry_worker_test.exs
@@ -0,0 +1,574 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.SignatureRetryWorkerTest do
+ use Pleroma.DataCase, async: false
+ use Oban.Testing, repo: Pleroma.Repo
+
+ import ExUnit.CaptureLog
+ import Pleroma.Factory
+
+ @moduletag capture_log: true
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.Signature
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Web.Federator
+ alias Pleroma.Workers.SignatureRetryWorker
+
+ defp signature_headers_for(%User{} = signer) do
+ [
+ {"host", "local.test"},
+ {"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
+ {"digest", "SHA-256=fake-digest"},
+ {"content-type", "application/activity+json"},
+ {
+ "signature",
+ "keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
+ }
+ ]
+ end
+
+ defp stub_actor_fetch(%User{} = signer) do
+ signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured")
+
+ Tesla.Mock.mock(fn
+ %{url: url} when url == signer.ap_id ->
+ %Tesla.Env{
+ status: 200,
+ body: Jason.encode!(signer_json),
+ headers: HttpRequestMock.activitypub_object_headers()
+ }
+ end)
+ end
+
+ defp expect_signature_from(%User{} = signer) do
+ stub_actor_fetch(signer)
+ Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
+ end
+
+ defp enqueue_failed_signature(params, signer) do
+ Federator.incoming_failed_signature_ap_doc(%{
+ method: "POST",
+ req_headers: signature_headers_for(signer),
+ request_path: "/inbox",
+ params: params,
+ query_string: ""
+ })
+ end
+
+ defp failed_signature_job(params, req_headers, opts \\ []) do
+ %Oban.Job{
+ args: %{
+ "op" => "incoming_failed_signature_ap_doc",
+ "method" => Keyword.get(opts, :method, "POST"),
+ "req_headers" => req_headers,
+ "request_path" => Keyword.get(opts, :request_path, "/inbox"),
+ "params" => params,
+ "query_string" => Keyword.get(opts, :query_string, "")
+ }
+ }
+ end
+
+ defp assert_mismatched_signature_cancelled(params, signer) do
+ assert {:ok, oban_job} = enqueue_failed_signature(params, signer)
+
+ capture_log([level: :warning], fn ->
+ assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job)
+ end)
+ end
+
+ test "Federator preserves request metadata for failed-signature retry jobs" do
+ params = insert(:note_activity).data
+
+ req_headers = [
+ {"host", "local.test"},
+ {"signature", "keyId=\"https://one.com/users/alice#main-key\""}
+ ]
+
+ assert {:ok, oban_job} =
+ Federator.incoming_failed_signature_ap_doc(%{
+ method: "POST",
+ req_headers: req_headers,
+ request_path: "/inbox",
+ params: params,
+ query_string: "foo=bar"
+ })
+
+ assert oban_job.worker == "Pleroma.Workers.SignatureRetryWorker"
+
+ assert %{
+ "op" => "incoming_failed_signature_ap_doc",
+ "method" => "POST",
+ "req_headers" => ^req_headers,
+ "request_path" => "/inbox",
+ "params" => ^params,
+ "query_string" => "foo=bar"
+ } = oban_job.args
+ end
+
+ test "cancels retry jobs without request metadata" do
+ params = insert(:note_activity).data
+
+ log =
+ capture_log([level: :warning], fn ->
+ assert {:cancel, :missing_signature_retry_metadata} =
+ SignatureRetryWorker.perform(%Oban.Job{
+ args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params}
+ })
+ end)
+
+ assert log =~ "Failed-signature inbox retry rejected"
+ assert log =~ "reason=:missing_signature_retry_metadata"
+ assert log =~ "payload_actor=#{inspect(params["actor"])}"
+ assert log =~ "activity_id=#{inspect(params["id"])}"
+ assert log =~ "type=#{inspect(params["type"])}"
+ assert log =~ "request_path=nil"
+ end
+
+ test "cancels retry jobs with malformed serialized request headers" do
+ params = insert(:note_activity).data
+
+ log =
+ capture_log([level: :warning], fn ->
+ assert {:cancel, :invalid_signature_retry_metadata} =
+ SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]]))
+ end)
+
+ assert log =~ "Failed-signature inbox retry rejected"
+ assert log =~ "reason=:invalid_signature_retry_metadata"
+ assert log =~ "signature_actor=nil"
+ assert log =~ "request_path=\"/inbox\""
+ end
+
+ test "cancels retry jobs without a signature header" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ params = insert(:note_activity, user: alice).data
+
+ log =
+ capture_log([level: :warning], fn ->
+ assert {:cancel, :invalid_signature} =
+ SignatureRetryWorker.perform(
+ failed_signature_job(params, [{"host", "local.test"}])
+ )
+ end)
+
+ assert log =~ "Failed-signature inbox retry rejected"
+ assert log =~ "reason=:invalid_signature"
+ assert log =~ "payload_actor=#{inspect(params["actor"])}"
+ assert log =~ "signature_actor=nil"
+ assert log =~ "request_path=\"/inbox\""
+ end
+
+ test "cancels missing signature before fetching an unavailable payload actor" do
+ params =
+ insert(:note_activity).data
+ |> Map.put("actor", "https://unavailable.example/users/bob")
+
+ assert {:cancel, :invalid_signature} =
+ SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}]))
+ end
+
+ test "cancels signer mismatch before fetching an unavailable payload actor" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+
+ params =
+ insert(:note_activity).data
+ |> Map.put("actor", "https://unavailable.example/users/bob")
+
+ assert {:cancel, :actor_signature_mismatch} =
+ SignatureRetryWorker.perform(
+ failed_signature_job(params, signature_headers_for(alice))
+ )
+ end
+
+ test "cancels retry jobs with a signature header without keyId" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ params = insert(:note_activity, user: alice).data
+
+ req_headers = [{"signature", "algorithm=\"rsa-sha256\",signature=\"fake-signature\""}]
+
+ assert {:cancel, :invalid_signature} =
+ SignatureRetryWorker.perform(failed_signature_job(params, req_headers))
+ end
+
+ test "cancels retry jobs with an unparsable signature keyId" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ params = insert(:note_activity, user: alice).data
+ req_headers = [{"signature", "keyId=\"not an activitypub id\",signature=\"fake-signature\""}]
+
+ assert {:cancel, :invalid_signature} =
+ SignatureRetryWorker.perform(failed_signature_job(params, req_headers))
+ end
+
+ test "cancels when the refetched key still cannot validate the signature" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+
+ create = %{
+ "type" => "Create",
+ "actor" => alice.ap_id,
+ "id" => "https://one.com/activities/invalid-signature-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://one.com/objects/invalid-signature-note",
+ "actor" => alice.ap_id,
+ "attributedTo" => alice.ap_id,
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ stub_actor_fetch(alice)
+
+ assert {:ok, oban_job} = enqueue_failed_signature(create, alice)
+
+ log =
+ capture_log([level: :warning], fn ->
+ assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job)
+ end)
+
+ assert log =~ "Failed-signature inbox retry rejected"
+ assert log =~ "reason=:invalid_signature"
+ assert log =~ "payload_actor=\"https://one.com/users/alice\""
+ assert log =~ "signature_actor=\"https://one.com/users/alice\""
+ assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\""
+ assert log =~ "type=\"Create\""
+ assert log =~ "request_path=\"/inbox\""
+
+ refute Activity.get_by_ap_id(create["id"])
+ end
+
+ test "processes the activity after refetching a valid matching signature" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+
+ create = %{
+ "type" => "Create",
+ "actor" => alice.ap_id,
+ "id" => "https://one.com/activities/valid-signature-create",
+ "context" => "https://one.com/contexts/valid-signature-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://one.com/objects/valid-signature-note",
+ "actor" => alice.ap_id,
+ "attributedTo" => alice.ap_id,
+ "context" => "https://one.com/contexts/valid-signature-create",
+ "content" => "valid post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ expect_signature_from(alice)
+
+ assert {:ok, oban_job} = enqueue_failed_signature(create, alice)
+ assert {:ok, %Activity{}} = SignatureRetryWorker.perform(oban_job)
+ assert Activity.get_by_ap_id(create["id"])
+ end
+
+ test "processes the activity when a real signature validates with a query string" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+
+ create = %{
+ "type" => "Create",
+ "actor" => alice.ap_id,
+ "id" => "https://one.com/activities/valid-query-signature-create",
+ "context" => "https://one.com/contexts/valid-query-signature-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://one.com/objects/valid-query-signature-note",
+ "actor" => alice.ap_id,
+ "attributedTo" => alice.ap_id,
+ "context" => "https://one.com/contexts/valid-query-signature-create",
+ "content" => "valid signed post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ stub_actor_fetch(alice)
+
+ date = "Thu, 25 Jul 2024 13:33:31 GMT"
+ digest = "SHA-256=fake-digest"
+
+ signature =
+ Signature.sign(alice, %{
+ "(request-target)" => "post /inbox?foo=bar",
+ "content-type" => "application/activity+json",
+ date: date,
+ digest: digest,
+ host: "local.test"
+ })
+
+ req_headers = [
+ ["host", "local.test"],
+ ["date", date],
+ ["digest", digest],
+ ["content-type", "application/activity+json"],
+ ["signature", signature]
+ ]
+
+ assert {:ok, %Activity{}} =
+ SignatureRetryWorker.perform(
+ failed_signature_job(create, req_headers, query_string: "foo=bar")
+ )
+
+ assert Activity.get_by_ap_id(create["id"])
+ end
+
+ test "cancels when signature actor does not match payload actor" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+
+ note =
+ insert(:note,
+ user: bob,
+ object_local: false,
+ data: %{"id" => "https://two.com/objects/malicious-update-note"}
+ )
+
+ update = %{
+ "type" => "Update",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/malicious-update",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data
+ }
+
+ assert_mismatched_signature_cancelled(update, alice)
+ end
+
+ test "cancels signature actor mismatch through Federator-created jobs" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+
+ note =
+ insert(:note,
+ user: bob,
+ object_local: false,
+ data: %{"id" => "https://two.com/objects/federator-malicious-note"}
+ )
+
+ update = %{
+ "type" => "Update",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/federator-malicious-update",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data
+ }
+
+ assert_mismatched_signature_cancelled(update, alice)
+ end
+
+ test "cancels signature actor mismatch before processing a forged Create" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+
+ create = %{
+ "type" => "Create",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://two.com/objects/forged-note",
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ assert_mismatched_signature_cancelled(create, alice)
+ end
+
+ test "cancels signature actor mismatch when payload actor is embedded" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+
+ create = %{
+ "type" => "Create",
+ "actor" => %{"id" => bob.ap_id},
+ "id" => "https://two.com/activities/embedded-actor-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://two.com/objects/embedded-actor-forged-note",
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ assert_mismatched_signature_cancelled(create, alice)
+ end
+
+ test "logs signature actor mismatch retry rejections" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+
+ create = %{
+ "type" => "Create",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/logged-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => "https://two.com/objects/logged-forged-note",
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ log = assert_mismatched_signature_cancelled(create, alice)
+
+ assert log =~ "Failed-signature inbox retry rejected"
+ assert log =~ "reason=:actor_signature_mismatch"
+ assert log =~ "payload_actor=\"https://two.com/users/bob\""
+ assert log =~ "signature_actor=\"https://one.com/users/alice\""
+ assert log =~ "activity_id=\"https://two.com/activities/logged-forged-create\""
+ assert log =~ "type=\"Create\""
+ assert log =~ "request_path=\"/inbox\""
+ end
+
+ test "cancels signature actor mismatch before actually creating a forged post" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+
+ object_id = "https://two.com/objects/actually-forged-note"
+
+ create = %{
+ "type" => "Create",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/actually-forged-create",
+ "context" => "https://two.com/contexts/actually-forged-create",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "id" => object_id,
+ "actor" => bob.ap_id,
+ "attributedTo" => bob.ap_id,
+ "context" => "https://two.com/contexts/actually-forged-create",
+ "content" => "forged post",
+ "published" => "2024-07-25T13:33:31Z",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => []
+ }
+ }
+
+ assert_mismatched_signature_cancelled(create, alice)
+ refute Object.get_by_ap_id(object_id)
+ end
+
+ test "cancels signature actor mismatch before processing a forged Like" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+ note = insert(:note)
+
+ like = %{
+ "type" => "Like",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/forged-like",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data["id"]
+ }
+
+ assert_mismatched_signature_cancelled(like, alice)
+ end
+
+ test "cancels signature actor mismatch before actually creating a forged Like" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+ note = insert(:note)
+
+ like = %{
+ "type" => "Like",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/actually-forged-like",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data["id"]
+ }
+
+ assert_mismatched_signature_cancelled(like, alice)
+ refute Activity.get_by_ap_id(like["id"])
+ end
+
+ test "cancels signature actor mismatch before processing a forged Announce" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+ note = insert(:note)
+
+ announce = %{
+ "type" => "Announce",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/forged-announce",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => note.data["id"]
+ }
+
+ assert_mismatched_signature_cancelled(announce, alice)
+ end
+
+ test "cancels signature actor mismatch before processing a forged Follow" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+ followed = insert(:user)
+
+ follow = %{
+ "type" => "Follow",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/forged-follow",
+ "to" => [followed.ap_id],
+ "cc" => [],
+ "object" => followed.ap_id
+ }
+
+ assert_mismatched_signature_cancelled(follow, alice)
+ end
+
+ test "cancels signature actor mismatch before processing a forged Undo" do
+ alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
+ bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
+
+ undo = %{
+ "type" => "Undo",
+ "actor" => bob.ap_id,
+ "id" => "https://two.com/activities/forged-undo",
+ "to" => ["https://www.w3.org/ns/activitystreams#Public"],
+ "cc" => [],
+ "object" => "https://two.com/activities/existing-bob-activity"
+ }
+
+ assert_mismatched_signature_cancelled(undo, alice)
+ end
+end
diff --git a/tools/check-changelog b/tools/check-changelog
index 5952aefcc..d09a68895 100644
--- a/tools/check-changelog
+++ b/tools/check-changelog
@@ -1,14 +1,14 @@
#!/bin/sh
echo "adding ownership exception"
-git config --global --add safe.directory $(pwd)
+git config --global --add safe.directory "$(pwd)"
echo "looking for change log"
git remote add upstream https://git.pleroma.social/pleroma/pleroma.git
-git fetch upstream ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}:refs/remotes/upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
+git fetch upstream ${CI_COMMIT_TARGET_BRANCH}:refs/remotes/upstream/${CI_COMMIT_TARGET_BRANCH}
-git diff --raw --no-renames upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME HEAD -- changelog.d | \
+git diff --raw --no-renames upstream/${CI_COMMIT_TARGET_BRANCH} HEAD -- changelog.d | \
grep ' A\t' | grep '\.\(skip\|add\|remove\|fix\|security\|change\)$'
ret=$?