Compare commits
321 Commits
Author | SHA1 | Date |
---|---|---|
Floatingghost | c02e3432d9 | |
Floatingghost | 21a81e1111 | |
Floatingghost | 3738ab67bd | |
Floatingghost | f614bf2725 | |
Floatingghost | 7038b60ab5 | |
Weblate | 91d9d750c0 | |
Weblate | 3c07aa506d | |
Weblate | 64050b0fb5 | |
Weblate | 7babc11475 | |
Floatingghost | 828158ef49 | |
Floatingghost | c7276713e0 | |
floatingghost | 310c1b7e24 | |
floatingghost | 7da6f41718 | |
floatingghost | 53c67993bb | |
Oneric | 5bc64c5753 | |
Oneric | 5ee0fb18cb | |
Norm | 5b320616ca | |
Norm | 72c2d9f009 | |
Oneric | 12db5c23f2 | |
Oneric | a95af3ee4c | |
Oneric | 163cb1d5e0 | |
Oneric | 24e608ab5b | |
Oneric | b0a46c1e2e | |
floatingghost | b1c6621e66 | |
floatingghost | 764dbeded4 | |
floatingghost | 06847ca5f8 | |
floatingghost | 80e1c094c7 | |
floatingghost | 4a0e90e8a8 | |
floatingghost | 1e48a37545 | |
Oneric | 83f75c3e93 | |
floatingghost | 7d89dba528 | |
Floatingghost | 92168fa5a1 | |
Floatingghost | 3e199242b0 | |
Norm | 0fa3fbf55e | |
Norm | e5f4282cca | |
Norm | cdde95ad8b | |
Norm | c493769364 | |
Norm | 39b8e73532 | |
Norm | 5405828ab1 | |
Norm | 3e9643b172 | |
Oneric | 20c22eb159 | |
Haelwenn (lanodan) Monnier | 0c2f200b4d | |
timorl | 3f54945033 | |
timorl | 09d3ccf770 | |
timorl | 9da0fe930e | |
timorl | 2a9db73b4c | |
floatingghost | 0fee71f58f | |
Floatingghost | 370576474c | |
Floatingghost | 1ed975636b | |
timorl | cd7af81896 | |
Floatingghost | 2c7e5b2287 | |
Floatingghost | ddb8a5ef73 | |
Floatingghost | 123db1abc4 | |
Floatingghost | b2c29527fb | |
timorl | 59d32c10d9 | |
Floatingghost | d2cee15c15 | |
Floatingghost | d70fa16383 | |
Floatingghost | 5043571084 | |
Floatingghost | 1896ff1ab0 | |
Floatingghost | b7dd739de1 | |
timorl | b144218dce | |
Floatingghost | 2fc25980d1 | |
floatingghost | c1f0b6b875 | |
Floatingghost | 18442dcc7e | |
Floatingghost | 33fb74043d | |
Floatingghost | 49ed27cd96 | |
Floatingghost | 7f6e35ece4 | |
Mark Felder | 2e369aef71 | |
Mark Felder | fed7a78c77 | |
Mark Felder | c0532bcae0 | |
Mark Felder | f31b262aec | |
Mark Felder | ff515c05c3 | |
Mark Felder | 7e5004b3e2 | |
Mark Felder | 53a9413b95 | |
Mark Felder | d69cba1b93 | |
Mark Felder | 3c54f407c5 | |
Mark Felder | 825ae46bfa | |
Mark Felder | 331710b6bb | |
Mark Felder | eeed051a0f | |
Mark Felder | 30d63aaa6e | |
Mark Felder | e2b04fac5a | |
Mark Felder | 6d368808d3 | |
Mark Felder | 160d113b30 | |
Mark Felder | 132036f951 | |
Mark Felder | 4ff22a409a | |
Mark Felder | 4c29366fe5 | |
Mark Felder | ac4cc619ea | |
Mark Felder | c241b5b09f | |
Floatingghost | f8a53fbe2f | |
floatingghost | e36c0f96fc | |
floatingghost | 6f3c955aa0 | |
floatingghost | 024ffadd80 | |
floatingghost | e2e4f53585 | |
Floatingghost | d910e8d7d1 | |
Floatingghost | df25d86999 | |
floatingghost | 4887df12d7 | |
floatingghost | e6ca2b4d2a | |
floatingghost | 6ba80aaff5 | |
floatingghost | 8e60177466 | |
Erin Shepherd | 75d9e2b375 | |
Floatingghost | 05f8179d08 | |
Oneric | fae0a14ee8 | |
Floatingghost | 1135935cbe | |
floatingghost | 090a77d1af | |
floatingghost | 0e066bddae | |
Oneric | bd74ad9ce4 | |
Oneric | 462225880a | |
Oneric | debd686418 | |
Oneric | 9598137d32 | |
floatingghost | b8393ad9ed | |
floatingghost | 554f19a9ed | |
FloatingGhost | 26a91d5c9e | |
FloatingGhost | 9c53a3390e | |
FloatingGhost | 795524daf1 | |
FloatingGhost | b5d97e7d85 | |
FloatingGhost | f592090206 | |
FloatingGhost | 61621ebdbc | |
FloatingGhost | 4cd299bd83 | |
Erin Shepherd | 8fbd771d6e | |
Erin Shepherd | 464db9ea0b | |
FloatingGhost | d71d52302c | |
FloatingGhost | 2d439034ca | |
FloatingGhost | 11c305b64b | |
FloatingGhost | 087d88f787 | |
FloatingGhost | 14515d8d4a | |
FloatingGhost | 3650bb0370 | |
Oneric | ee7d98b093 | |
Oneric | 0648d9ebaa | |
Oneric | d441101200 | |
Oneric | 31f90bbb52 | |
Oneric | 61ec592d66 | |
Oneric | 8684964c5d | |
Oneric | 48b3a35793 | |
Oneric | 9061d148be | |
Oneric | 3e134b07fa | |
Oneric | f07eb4cb55 | |
Oneric | 59a142e0b0 | |
Oneric | fee57eb376 | |
Oneric | c4cf4d7f0b | |
Oneric | baaeffdebc | |
Oneric | 2bcf633dc2 | |
Oneric | 93ab6a018e | |
Oneric | c806adbfdb | |
Oneric | ddd79ff22d | |
Oneric | d6d838cbe8 | |
Oneric | 6d003e1acd | |
Oneric | d1ce5fd911 | |
Oneric | a4fa2ec9af | |
Oneric | ee5ce87825 | |
Oneric | d1c4d07404 | |
Oneric | fa98b44acf | |
Oneric | 5b126567bb | |
Oneric | a8c6c780b4 | |
Oneric | 111cdb0d86 | |
Norm | af041db6dc | |
Oneric | fb54c47f0b | |
Oneric | fc36b04016 | |
Oneric | 11ae8344eb | |
Oneric | bcc528b2e2 | |
Oneric | e88d0a2853 | |
Oneric | ba558c0c24 | |
Oneric | 0ec62acb9d | |
Oneric | fef773ca35 | |
Oneric | bdefbb8fd9 | |
Oneric | f7c9793542 | |
Sandra Snan | 6116f81546 | |
Oneric | 7ef93c0b6d | |
Oneric | dbb6091d01 | |
Oneric | 5d467af6c5 | |
Helge | 5d89e0c917 | |
Erin Shepherd | f18e2ba42c | |
Oneric | fc95519dbf | |
FloatingGhost | a03f3a9d89 | |
FloatingGhost | 889b57df82 | |
Weblate | 34ffb92db4 | |
Weblate | c6dceb1802 | |
Weblate | caaf2deb22 | |
floatingghost | 7d61fb0906 | |
floatingghost | cdf73e0ac8 | |
floatingghost | 967e6b8ade | |
Oneric | d7c8e9df27 | |
Oneric | a0daec6ea1 | |
Oneric | bff2812a93 | |
Oneric | 7964272c98 | |
Oneric | c08f49d88e | |
FloatingGhost | 3111181d3c | |
floatingghost | 2f9aad0e65 | |
Erin Shepherd | b387f4a1c1 | |
Haelwenn (lanodan) Monnier | 7d94476dd6 | |
rick | c25cfe9b7a | |
Oneric | 41dd37d796 | |
Oneric | 9830d54fa1 | |
Oneric | f254e4f530 | |
Oneric | da4190c46e | |
Oneric | 7a2d68c3ab | |
Oneric | 8e7a89605d | |
Oneric | 1640d19448 | |
Oneric | 8f1776a8a7 | |
Oneric | 1ec6e193e6 | |
Oneric | 37e2a35b86 | |
floatingghost | 086d6100e1 | |
floatingghost | 3e24210e9f | |
floatingghost | 551ae69541 | |
YokaiRick | 37f9626116 | |
stefan230 | b4c832471c | |
rick | db49daa4a5 | |
rick | 718104117f | |
rick | 12e7d0a25c | |
Oneric | 1a7839eaf2 | |
Oneric | 1ef8b967d2 | |
Erin Shepherd | 7a0e27a746 | |
floatingghost | 755c75d8a4 | |
floatingghost | 289f93f5a2 | |
floatingghost | 371b258c99 | |
Oneric | 3b0714c4fd | |
floatingghost | 34c213f02f | |
Oneric | e99e2407f3 | |
Oneric | 7622aa27ca | |
FloatingGhost | 0ed815b8a1 | |
floatingghost | c5dcd07e08 | |
floatingghost | 874ee73a87 | |
floatingghost | a905223837 | |
Oneric | cda597a05c | |
Oneric | 711043f57d | |
Oneric | 6bb455702d | |
Oneric | 7493d8f49d | |
Haelwenn (lanodan) Monnier | cb7eaccecb | |
Oneric | 376f6b15ca | |
Oneric | 13e62b4e51 | |
floatingghost | 6fde75e1f0 | |
Oneric | 192480093c | |
Oneric | 29f564f700 | |
Oneric | 16197ff57a | |
Oneric | 8f8e1ff214 | |
Oneric | 18ecae6183 | |
Oneric | a6df71eebb | |
Oneric | 8cf183cb42 | |
Oneric | 5f7d47dcb7 | |
Paweł Świątkowski | df21b61829 | |
floatingghost | e97d08ee98 | |
Paweł Świątkowski | d7d159c49f | |
Oneric | 3cd882528e | |
Oneric | e47c50666d | |
floatingghost | b4ccddab39 | |
Aria | 77000b8ffd | |
Aria | a074be24ca | |
Aria | eb0dbf6b79 | |
Aria | e2f749b5b0 | |
FloatingGhost | 6fb91d79f3 | |
FloatingGhost | 2858cd81e1 | |
Lain Soykaf | c3098e9c56 | |
Yonle | 8a0e797cff | |
FloatingGhost | 74d5e22fc5 | |
floatingghost | bc22ea50ab | |
floatingghost | 8ae5364886 | |
FloatingGhost | 6cc523bd23 | |
FloatingGhost | fb700a956a | |
floatingghost | c12d158491 | |
floatingghost | ed5c930dd9 | |
floatingghost | 3cca953c58 | |
Rohan Kumar | 36f4f18aa5 | |
FloatingGhost | 033b7b04e0 | |
FloatingGhost | d1af78aba1 | |
FloatingGhost | 3e7446d177 | |
floatingghost | ebfb617b26 | |
FloatingGhost | c8e08e9cc3 | |
Koneko Toujou | 1b9edcba64 | |
yuki joou | 32422a7a04 | |
y0nei | 0617090743 | |
FloatingGhost | 5c164028cf | |
FloatingGhost | f7ea0a1248 | |
FloatingGhost | 6139c3346d | |
YokaiRick | 6ec5437294 | |
FloatingGhost | 0af8e93135 | |
FloatingGhost | 98a64ab145 | |
FloatingGhost | 94d1af2c4c | |
FloatingGhost | 98f0820ca4 | |
FloatingGhost | 9bc0345e57 | |
FloatingGhost | f3cc60b202 | |
FloatingGhost | 063e3c0d34 | |
FloatingGhost | 6cb40bee26 | |
floatingghost | 7a3529ec1c | |
YokaiRick | 76ba400c6d | |
YokaiRick | 655c282de3 | |
floatingghost | 0b32beb051 | |
floatingghost | 7bb41bffb3 | |
floatingghost | fd11e4f8cd | |
FloatingGhost | 1bd3012c2d | |
floatingghost | 2df7707060 | |
Joshua Goins | c22ecac567 | |
Norm | d79c92f9c6 | |
FloatingGhost | 7bd4ae5412 | |
FloatingGhost | 165c2485ff | |
FloatingGhost | 60a07da5ef | |
FloatingGhost | 73be5c3f30 | |
FloatingGhost | 43c5fd5db0 | |
FloatingGhost | e7788f3c82 | |
Norm | 0cb3812ac0 | |
FloatingGhost | f2da47679d | |
FloatingGhost | 63a5b8506c | |
FloatingGhost | c7aeeec232 | |
FloatingGhost | 80cbdc8480 | |
floatingghost | 3f1e2b0b3b | |
FloatingGhost | 0c21341156 | |
Clovis | fc3cc61768 | |
FloatingGhost | 7825798e32 | |
FloatingGhost | e59fc0677b | |
FloatingGhost | 650c0c0f62 | |
FloatingGhost | 7956cfb091 | |
FloatingGhost | 215b550317 | |
FloatingGhost | c193b4d507 | |
FloatingGhost | 866672b6a7 | |
FloatingGhost | ef422a8385 | |
FloatingGhost | 9723264fe5 | |
FloatingGhost | c887dd4f2e | |
FloatingGhost | 368b22fd2f | |
Norm | 9a7c30fc90 | |
Norm | b99053d2c2 | |
Ilja | 66a04cead3 | |
Ilja | f50cffd134 | |
Ilja | 338612d72b |
|
@ -1,3 +1,14 @@
|
|||
[
|
||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/repo/migrations/*.exs", "priv/repo/optional_migrations/**/*.exs", "priv/scrubbers/*.ex"]
|
||||
import_deps: [:ecto, :ecto_sql, :phoenix],
|
||||
subdirectories: ["priv/*/migrations"],
|
||||
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||
inputs: [
|
||||
"mix.exs",
|
||||
"*.{heex,ex,exs}",
|
||||
"{config,lib,test}/**/*.{heex,ex,exs}",
|
||||
"priv/*/seeds.exs",
|
||||
"priv/repo/migrations/*.exs",
|
||||
"priv/repo/optional_migrations/**/*.exs",
|
||||
"priv/scrubbers/*.ex"
|
||||
]
|
||||
]
|
||||
|
|
|
@ -78,3 +78,4 @@ docs/venv
|
|||
# docker stuff
|
||||
docker-db
|
||||
*.iml
|
||||
docker-compose.override.yml
|
||||
|
|
|
@ -37,7 +37,7 @@ variables:
|
|||
pipeline:
|
||||
# Canonical amd64
|
||||
debian-bookworm:
|
||||
image: hexpm/elixir:1.15.4-erlang-25.3.2.5-debian-bookworm-20230612
|
||||
image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612
|
||||
<<: *on-release
|
||||
environment:
|
||||
MIX_ENV: prod
|
||||
|
@ -65,9 +65,36 @@ pipeline:
|
|||
- export DEST=scaleway:akkoma-updates/$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}/akkoma-amd64-ubuntu-jammy.zip
|
||||
- /bin/sh /entrypoint.sh
|
||||
|
||||
debian-bullseye:
|
||||
image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bullseye-20230612
|
||||
<<: *on-release
|
||||
environment:
|
||||
MIX_ENV: prod
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
commands:
|
||||
- apt-get update && apt-get install -y cmake libmagic-dev rclone zip imagemagick libmagic-dev git build-essential g++ wget
|
||||
- *clean
|
||||
- echo "import Config" > config/prod.secret.exs
|
||||
- *setup-hex
|
||||
- *mix-clean
|
||||
- *tag-build
|
||||
- mix deps.get --only prod
|
||||
- mix release --path release
|
||||
- zip akkoma-amd64-debian-bullseye.zip -r release
|
||||
|
||||
release-debian-bullseye:
|
||||
image: akkoma/releaser
|
||||
<<: *on-release
|
||||
secrets: *scw-secrets
|
||||
commands:
|
||||
- export SOURCE=akkoma-amd64-debian-bullseye.zip
|
||||
# AMD64
|
||||
- export DEST=scaleway:akkoma-updates/$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}/akkoma-amd64-debian-bullseye.zip
|
||||
- /bin/sh /entrypoint.sh
|
||||
|
||||
# Canonical amd64-musl
|
||||
musl:
|
||||
image: hexpm/elixir:1.14.3-erlang-25.2.2-alpine-3.18.0
|
||||
image: hexpm/elixir:1.15.4-erlang-26.0.2-alpine-3.18.2
|
||||
<<: *on-stable
|
||||
environment:
|
||||
MIX_ENV: prod
|
||||
|
|
|
@ -37,7 +37,7 @@ variables:
|
|||
pipeline:
|
||||
# Canonical arm64
|
||||
debian-bookworm:
|
||||
image: hexpm/elixir:1.15.4-erlang-25.3.2.5-debian-bookworm-20230612
|
||||
image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612
|
||||
<<: *on-release
|
||||
environment:
|
||||
MIX_ENV: prod
|
||||
|
@ -65,7 +65,7 @@ pipeline:
|
|||
|
||||
# Canonical arm64-musl
|
||||
musl:
|
||||
image: hexpm/elixir:1.15.4-erlang-25.3.2.5-alpine-3.18.2
|
||||
image: hexpm/elixir:1.15.4-erlang-26.0.2-alpine-3.18.2
|
||||
<<: *on-stable
|
||||
environment:
|
||||
MIX_ENV: prod
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
platform: linux/amd64
|
||||
|
||||
variables:
|
||||
- &scw-secrets
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
- &setup-hex "mix local.hex --force && mix local.rebar --force"
|
||||
- &on-release
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- tag
|
||||
branch:
|
||||
- develop
|
||||
- stable
|
||||
- refs/tags/v*
|
||||
- refs/tags/stable-*
|
||||
- &on-stable
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- tag
|
||||
branch:
|
||||
- stable
|
||||
- refs/tags/stable-*
|
||||
- &on-point-release
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
- develop
|
||||
- stable
|
||||
- &on-pr-open
|
||||
when:
|
||||
event:
|
||||
- pull_request
|
||||
|
||||
- &tag-build "export BUILD_TAG=$${CI_COMMIT_TAG:-\"$CI_COMMIT_BRANCH\"} && export PLEROMA_BUILD_BRANCH=$BUILD_TAG"
|
||||
|
||||
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
||||
- &mix-clean "mix deps.clean --all && mix clean"
|
||||
|
||||
pipeline:
|
||||
lint:
|
||||
image: akkoma/ci-base:1.15-otp26
|
||||
<<: *on-pr-open
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
commands:
|
||||
- mix local.hex --force
|
||||
- mix local.rebar --force
|
||||
- mix deps.get
|
||||
- mix compile
|
||||
- mix format --check-formatted
|
|
@ -1,9 +1,13 @@
|
|||
platform: linux/amd64
|
||||
|
||||
depends_on:
|
||||
- lint
|
||||
|
||||
matrix:
|
||||
ELIXIR_VERSION:
|
||||
- 1.14
|
||||
- 1.15
|
||||
- 1.16
|
||||
OTP_VERSION:
|
||||
- 25
|
||||
- 26
|
||||
|
@ -12,9 +16,10 @@ matrix:
|
|||
OTP_VERSION: 25
|
||||
- ELIXIR_VERSION: 1.15
|
||||
OTP_VERSION: 25
|
||||
# Soon
|
||||
#- ELIXIR_VERSION: 1.15
|
||||
# OTP_VERSION: 26
|
||||
- ELIXIR_VERSION: 1.15
|
||||
OTP_VERSION: 26
|
||||
- ELIXIR_VERSION: 1.16
|
||||
OTP_VERSION: 26
|
||||
|
||||
variables:
|
||||
- &scw-secrets
|
||||
|
@ -69,15 +74,7 @@ services:
|
|||
POSTGRES_PASSWORD: postgres
|
||||
|
||||
pipeline:
|
||||
lint:
|
||||
<<: *on-pr-open
|
||||
image: akkoma/ci-base:1.15
|
||||
commands:
|
||||
- mix local.hex --force
|
||||
- mix local.rebar --force
|
||||
- mix format --check-formatted
|
||||
|
||||
build:
|
||||
test:
|
||||
image: akkoma/ci-base:${ELIXIR_VERSION}-otp${OTP_VERSION}
|
||||
<<: *on-pr-open
|
||||
environment:
|
||||
|
@ -91,24 +88,9 @@ pipeline:
|
|||
- mix local.rebar --force
|
||||
- mix deps.get
|
||||
- mix compile
|
||||
|
||||
test:
|
||||
image: akkoma/ci-base:${ELIXIR_VERSION}-otp${OTP_VERSION}
|
||||
<<: *on-pr-open
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
POSTGRES_DB: pleroma_test_${ELIXIR_VERSION}_${OTP_VERSION}
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
DB_HOST: postgres
|
||||
commands:
|
||||
- mix local.hex --force
|
||||
- mix local.rebar --force
|
||||
- mix deps.get
|
||||
- mix compile
|
||||
- mix ecto.drop -f -q
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- mkdir -p test/tmp
|
||||
- mix test --preload-modules --exclude erratic --exclude federated --exclude mocked
|
||||
- mix test --preload-modules --only mocked
|
||||
- mix ecto.drop -f -q
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- mkdir -p test/tmp
|
||||
- mix test --preload-modules --exclude erratic --exclude federated --exclude mocked
|
||||
- mix test --preload-modules --only mocked
|
||||
|
|
100
CHANGELOG.md
100
CHANGELOG.md
|
@ -4,6 +4,106 @@ 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/).
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 2024.04
|
||||
|
||||
## Added
|
||||
- Support for [FEP-fffd](https://codeberg.org/fediverse/fep/src/branch/main/fep/fffd/fep-fffd.md) (proxy objects)
|
||||
- Verified support for elixir 1.16
|
||||
- Uploadfilter `Pleroma.Upload.Filter.Exiftool.ReadDescription` returns description values to the FE so they can pre fill the image description field
|
||||
NOTE: this filter MUST be placed before `Exiftool.StripMetadata` to work
|
||||
|
||||
## Changed
|
||||
- Inbound pipeline error handing was modified somewhat, which should lead to less incomprehensible log spam. Hopefully.
|
||||
- Uploadfilter `Pleroma.Upload.Filter.Exiftool` was replaced by `Pleroma.Upload.Filter.Exiftool.StripMetadata`;
|
||||
the latter strips all non-essential metadata by default but can be configured.
|
||||
To regain the old behaviour of only stripping GPS data set `purge: ["gps:all"]`.
|
||||
- Uploadfilter `Pleroma.Upload.Filter.Exiftool` has been renamed to `Pleroma.Upload.Filter.Exiftool.StripMetadata`
|
||||
- MRF.InlineQuotePolicy now prefers to insert display URLs instead of ActivityPub IDs
|
||||
- Old accounts are no longer listed in WebFinger as aliases; this was breaking spec
|
||||
|
||||
## Fixed
|
||||
- Issue preventing fetching anything from IPv6-only instances
|
||||
- Issue allowing post content to leak via opengraph tags despite :estrict\_unauthenticated being set
|
||||
- Move activities no longer operate on stale user data
|
||||
- Missing definitions in our JSON-LD context
|
||||
- Issue mangling newlines in code blocks for RSS/Atom feeds
|
||||
- static\_fe squeezing non-square avatars and emoji
|
||||
- Issue leading to properly JSON-LD compacted emoji reactions being rejected
|
||||
- We now use a standard-compliant Accept header when fetching ActivityPub objects
|
||||
- /api/pleroma/notification\_settings was rejecting body parameters;
|
||||
this also broke changing this setting via akkoma-fe
|
||||
- Issue leading to Mastodon bot accounts being rejected
|
||||
- Scope misdetection of remote posts resulting from not recognising
|
||||
JSON-LD-compacted forms of public scope; affected e.g. federation with bovine
|
||||
|
||||
## Removed
|
||||
- ActivityPub Client-To-Server write API endpoints have been disabled;
|
||||
read endpoints are planned to be removed next release unless a clear need is demonstrated
|
||||
|
||||
## 2024.03
|
||||
|
||||
## Added
|
||||
- CLI tasks best-effort checking for past abuse of the recent spoofing exploit
|
||||
- new `:mrf_steal_emoji, :download_unknown_size` option; defaults to `false`
|
||||
|
||||
## Changed
|
||||
- `Pleroma.Upload, :base_url` now MUST be configured explicitly if used;
|
||||
use of the same domain as the instance is **strongly** discouraged
|
||||
- `:media_proxy, :base_url` now MUST be configured explicitly if used;
|
||||
use of the same domain as the instance is **strongly** discouraged
|
||||
- StealEmoji:
|
||||
- now uses the pack.json format;
|
||||
existing users must migrate with an out-of-band script (check release notes)
|
||||
- only steals shortcodes recognised as valid
|
||||
- URLs of stolen emoji is no longer predictable
|
||||
- The `Dedupe` upload filter is now always active;
|
||||
`AnonymizeFilenames` is again opt-in
|
||||
- received AP data is sanity checked before we attempt to parse it as a user
|
||||
- Uploads, emoji and media proxy now restrict Content-Type headers to a safe subset
|
||||
- Akkoma will no longer fetch and parse objects hosted on the same domain
|
||||
|
||||
## Fixed
|
||||
- Critical security issue allowing Akkoma to be used as a vector for
|
||||
(depending on configuration) impersonation of other users or creation
|
||||
of bogus users and posts on the upload domain
|
||||
- Critical security issue letting Akkoma fall for the above impersonation
|
||||
payloads due to lack of strict id checking
|
||||
- Critical security issue allowing domains redirect to to pose as the initial domain
|
||||
(e.g. with media proxy's fallback redirects)
|
||||
- refetched objects can no longer attribute themselves to third-party actors
|
||||
(this had no externally visible effect since actor info is read from the Create activity)
|
||||
- our litepub JSON-LD schema is now served with the correct content type
|
||||
- remote APNG attachments are now recognised as images
|
||||
|
||||
## Upgrade Notes
|
||||
|
||||
- As mentioned in "Changed", `Pleroma.Upload, :base_url` **MUST** be configured. Uploads will fail without it.
|
||||
- Akkoma will refuse to start if this is not set.
|
||||
- Same with media proxy.
|
||||
|
||||
## 2024.02
|
||||
|
||||
## Added
|
||||
- Full compatibility with Erlang OTP26
|
||||
- handling of GET /api/v1/preferences
|
||||
- Akkoma API is now documented
|
||||
- ability to auto-approve follow requests from users you are already following
|
||||
- The SimplePolicy MRF can now strip user backgrounds from selected remote hosts
|
||||
|
||||
## Changed
|
||||
- OTP builds are now built on erlang OTP26
|
||||
- The base Phoenix framework is now updated to 1.7
|
||||
- An `outbox` field has been added to actor profiles to comply with AP spec
|
||||
- User profile backgrounds do now federate with other Akkoma instances and Sharkey
|
||||
|
||||
## Fixed
|
||||
- Documentation issue in which a non-existing nginx file was referenced
|
||||
- Issue where a bad inbox URL could break federation
|
||||
- Issue where hashtag rel values would be scrubbed
|
||||
- Issue where short domains listed in `transparency_obfuscate_domains` were not actually obfuscated
|
||||
|
||||
## 2023.08
|
||||
|
||||
## Added
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM hexpm/elixir:1.15.4-erlang-25.3.2.5-alpine-3.18.2
|
||||
FROM hexpm/elixir:1.15.4-erlang-26.0.2-alpine-3.18.2
|
||||
|
||||
ENV MIX_ENV=prod
|
||||
ENV ERL_EPMD_ADDRESS=127.0.0.1
|
||||
|
|
27
SECURITY.md
27
SECURITY.md
|
@ -1,16 +1,21 @@
|
|||
# Pleroma backend security policy
|
||||
|
||||
## Supported versions
|
||||
|
||||
Currently, Pleroma offers bugfixes and security patches only for the latest minor release.
|
||||
|
||||
| Version | Support
|
||||
|---------| --------
|
||||
| 2.2 | Bugfixes and security patches
|
||||
# Akkoma backend security handling
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please use confidential issues (tick the "This issue is confidential and should only be visible to team members with at least Reporter access." box when submitting) at our [bugtracker](https://git.pleroma.social/pleroma/pleroma/-/issues/new) for reporting vulnerabilities.
|
||||
Please send an email (preferably encrypted) or
|
||||
a DM via our IRC to one of the following people:
|
||||
|
||||
| Forgejo nick | IRC nick | Email | GPG |
|
||||
| ------------ | ------------- | ------------- | --------------------------------------- |
|
||||
| floatinghost | FloatingGhost | *see GPG key* | https://coffee-and-dreams.uk/pubkey.asc |
|
||||
|
||||
## Announcements
|
||||
|
||||
New releases are announced at [pleroma.social](https://pleroma.social/announcements/). All security releases are tagged with ["Security"](https://pleroma.social/announcements/tags/security/). You can be notified of them by subscribing to an Atom feed at <https://pleroma.social/announcements/tags/security/feed.xml>.
|
||||
New releases and security issues are announced at
|
||||
[meta.akkoma.dev](https://meta.akkoma.dev/c/releases) and
|
||||
[@akkoma@ihatebeinga.live](https://ihatebeinga.live/akkoma).
|
||||
|
||||
Both also offer RSS feeds
|
||||
([meta](https://meta.akkoma.dev/c/releases/7.rss),
|
||||
[fedi](https://ihatebeinga.live/users/akkoma.rss))
|
||||
so you can keep an eye on it without any accounts.
|
||||
|
|
|
@ -61,11 +61,12 @@ config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.
|
|||
# Upload configuration
|
||||
config :pleroma, Pleroma.Upload,
|
||||
uploader: Pleroma.Uploaders.Local,
|
||||
filters: [Pleroma.Upload.Filter.Dedupe],
|
||||
filters: [],
|
||||
link_name: false,
|
||||
proxy_remote: false,
|
||||
filename_display_max_length: 30,
|
||||
base_url: nil
|
||||
base_url: nil,
|
||||
allowed_mime_types: ["image", "audio", "video"]
|
||||
|
||||
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
|
||||
|
||||
|
@ -110,17 +111,6 @@ config :pleroma, :uri_schemes,
|
|||
"xmpp"
|
||||
]
|
||||
|
||||
websocket_config = [
|
||||
path: "/websocket",
|
||||
serializer: [
|
||||
{Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
|
||||
{Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
|
||||
],
|
||||
timeout: 60_000,
|
||||
transport_log: false,
|
||||
compress: false
|
||||
]
|
||||
|
||||
# Configures the endpoint
|
||||
config :pleroma, Pleroma.Web.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
|
@ -130,10 +120,7 @@ config :pleroma, Pleroma.Web.Endpoint,
|
|||
{:_,
|
||||
[
|
||||
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
|
||||
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
|
||||
{Phoenix.Transports.WebSocket,
|
||||
{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
|
||||
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
|
||||
{:_, Plug.Cowboy.Handler, {Pleroma.Web.Endpoint, []}}
|
||||
]}
|
||||
]
|
||||
],
|
||||
|
@ -162,14 +149,38 @@ config :logger, :ex_syslogger,
|
|||
format: "$metadata[$level] $message",
|
||||
metadata: [:request_id]
|
||||
|
||||
# ———————————————————————————————————————————————————————————————
|
||||
# W A R N I N G
|
||||
# ———————————————————————————————————————————————————————————————
|
||||
#
|
||||
# Whenever adding a privileged new custom type for e.g.
|
||||
# ActivityPub objects, ALWAYS map their extension back
|
||||
# to "application/octet-stream".
|
||||
# Else files served by us can automatically end up with
|
||||
# those privileged types causing severe security hazards.
|
||||
# (We need those mappings so Phoenix can assoiate its format
|
||||
# (the "extension") to incoming requests of those MIME types)
|
||||
#
|
||||
# ———————————————————————————————————————————————————————————————
|
||||
config :mime, :types, %{
|
||||
"application/xml" => ["xml"],
|
||||
"application/xrd+xml" => ["xrd+xml"],
|
||||
"application/jrd+json" => ["jrd+json"],
|
||||
"application/activity+json" => ["activity+json"],
|
||||
"application/ld+json" => ["activity+json"]
|
||||
"application/ld+json" => ["activity+json"],
|
||||
# Can be removed when bumping MIME past 2.0.5
|
||||
# see https://akkoma.dev/AkkomaGang/akkoma/issues/657
|
||||
"image/apng" => ["apng"]
|
||||
}
|
||||
|
||||
config :mime, :extensions, %{
|
||||
"xrd+xml" => "text/plain",
|
||||
"jrd+json" => "text/plain",
|
||||
"activity+json" => "text/plain"
|
||||
}
|
||||
|
||||
# ———————————————————————————————————————————————————————————————
|
||||
|
||||
config :tesla, :adapter, {Tesla.Adapter.Finch, name: MyFinch}
|
||||
|
||||
# Configures http settings, upstream proxy etc.
|
||||
|
@ -300,7 +311,6 @@ config :pleroma, :frontend_configurations,
|
|||
alwaysShowSubjectInput: true,
|
||||
background: "/images/city.jpg",
|
||||
collapseMessageWithSubject: false,
|
||||
disableChat: false,
|
||||
greentext: false,
|
||||
hideFilteredStatuses: false,
|
||||
hideMutedPosts: false,
|
||||
|
@ -388,6 +398,7 @@ config :pleroma, :mrf_simple,
|
|||
accept: [],
|
||||
avatar_removal: [],
|
||||
banner_removal: [],
|
||||
background_removal: [],
|
||||
reject_deletes: [],
|
||||
handle_threads: true
|
||||
|
||||
|
@ -416,8 +427,6 @@ config :pleroma, :mrf_object_age,
|
|||
threshold: 604_800,
|
||||
actions: [:delist, :strip_followers]
|
||||
|
||||
config :pleroma, :mrf_follow_bot, follower_nickname: nil
|
||||
|
||||
config :pleroma, :mrf_reject_newly_created_account_notes, age: 86_400
|
||||
|
||||
config :pleroma, :rich_media,
|
||||
|
@ -463,10 +472,6 @@ config :pleroma, :media_preview_proxy,
|
|||
image_quality: 85,
|
||||
min_content_length: 100 * 1024
|
||||
|
||||
config :pleroma, :shout,
|
||||
enabled: true,
|
||||
limit: 5_000
|
||||
|
||||
config :phoenix, :format_encoders, json: Jason, "activity+json": Jason
|
||||
|
||||
config :phoenix, :json_library, Jason
|
||||
|
|
|
@ -100,9 +100,22 @@ config :pleroma, :config_description, [
|
|||
label: "Base URL",
|
||||
type: :string,
|
||||
description:
|
||||
"Base URL for the uploads. Required if you use a CDN or host attachments under a different domain.",
|
||||
"Base URL for the uploads. Required if you use a CDN or host attachments under a different domain - it is HIGHLY recommended that you **do not** set this to be the same as the domain akkoma is hosted on.",
|
||||
suggestions: [
|
||||
"https://cdn-host.com"
|
||||
"https://media.akkoma.dev/media/"
|
||||
]
|
||||
},
|
||||
%{
|
||||
key: :allowed_mime_types,
|
||||
label: "Allowed MIME types",
|
||||
type: {:list, :string},
|
||||
description:
|
||||
"List of MIME (main) types uploads are allowed to identify themselves with. Other types may still be uploaded, but will identify as a generic binary to clients. WARNING: Loosening this over the defaults can lead to security issues. Removing types is safe, but only add to the list if you are sure you know what you are doing.",
|
||||
suggestions: [
|
||||
"image",
|
||||
"audio",
|
||||
"video",
|
||||
"font"
|
||||
]
|
||||
},
|
||||
%{
|
||||
|
@ -209,6 +222,26 @@ config :pleroma, :config_description, [
|
|||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: Pleroma.Upload.Filter.Exiftool.StripMetadata,
|
||||
type: :group,
|
||||
description: "Strip specified metadata from image uploads",
|
||||
children: [
|
||||
%{
|
||||
key: :purge,
|
||||
description: "Metadata fields or groups to strip",
|
||||
type: {:list, :string},
|
||||
suggestions: ["all", "CommonIFD0"]
|
||||
},
|
||||
%{
|
||||
key: :preserve,
|
||||
description: "Metadata fields or groups to preserve (takes precedence over stripping)",
|
||||
type: {:list, :string},
|
||||
suggestions: ["ColorSpaces", "Orientation"]
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
group: :pleroma,
|
||||
key: Pleroma.Emails.Mailer,
|
||||
|
|
|
@ -22,9 +22,12 @@ config :logger, :console,
|
|||
config :pleroma, :auth, oauth_consumer_strategies: []
|
||||
|
||||
config :pleroma, Pleroma.Upload,
|
||||
base_url: "http://localhost:4001/media/",
|
||||
filters: [],
|
||||
link_name: false
|
||||
|
||||
config :pleroma, :media_proxy, base_url: "http://localhost:4001"
|
||||
|
||||
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
|
||||
|
||||
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test, enabled: true
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"skip_files": [
|
||||
"test/support",
|
||||
"lib/mix/tasks/pleroma/benchmark.ex",
|
||||
"lib/credo/check/consistency/file_location.ex"
|
||||
]
|
||||
}
|
|
@ -4,6 +4,7 @@ services:
|
|||
db:
|
||||
image: akkoma-db:latest
|
||||
build: ./docker-resources/database
|
||||
shm_size: 4gb
|
||||
restart: unless-stopped
|
||||
user: ${DOCKER_USER}
|
||||
environment: {
|
||||
|
@ -45,7 +46,7 @@ services:
|
|||
volumes:
|
||||
- .:/opt/akkoma
|
||||
|
||||
# Uncomment the following if you want to use a reverse proxy
|
||||
# Copy this into docker-compose.override.yml and uncomment there if you want to use a reverse proxy
|
||||
#proxy:
|
||||
# image: caddy:2-alpine
|
||||
# restart: unless-stopped
|
||||
|
|
|
@ -37,7 +37,8 @@ If any of the options are left unspecified, you will be prompted interactively.
|
|||
- `--static-dir <path>` - the directory custom public files should be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)
|
||||
- `--listen-ip <ip>` - the ip the app should listen to, defaults to 127.0.0.1
|
||||
- `--listen-port <port>` - the port the app should listen to, defaults to 4000
|
||||
- `--strip-uploads <Y|N>` - use ExifTool to strip uploads of sensitive location data
|
||||
- `--strip-uploads-metadata <Y|N>` - use ExifTool to strip uploads of metadata when possible
|
||||
- `--read-uploads-description <Y|N>` - use ExifTool to read image descriptions from uploads
|
||||
- `--anonymize-uploads <Y|N>` - randomize uploaded filenames
|
||||
- `--dedupe-uploads <Y|N>` - store files based on their hash to reduce data storage requirements if duplicates are uploaded with different filenames
|
||||
- `--skip-release-env` - skip generation the release environment file
|
||||
|
|
|
@ -17,5 +17,5 @@ If you want to generate a restrictive `robots.txt`, you can run the following mi
|
|||
=== "From Source"
|
||||
|
||||
```sh
|
||||
mix pleroma.robotstxt disallow_all
|
||||
mix pleroma.robots_txt disallow_all
|
||||
```
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
# Security-related tasks
|
||||
|
||||
{! administration/CLI_tasks/general_cli_task_info.include !}
|
||||
|
||||
!!! danger
|
||||
Many of these tasks were written in response to a patched exploit.
|
||||
It is recommended to run those very soon after installing its respective security update.
|
||||
Over time with db migrations they might become less accurate or be removed altogether.
|
||||
If you never ran an affected version, there’s no point in running them.
|
||||
|
||||
## Spoofed AcitivityPub objects exploit (2024-03, fixed in 3.11.1)
|
||||
|
||||
### Search for uploaded spoofing payloads
|
||||
|
||||
Scans local uploads for spoofing payloads.
|
||||
If the instance is not using the local uploader it was not affected.
|
||||
Attachments wil be scanned anyway in case local uploader was used in the past.
|
||||
|
||||
!!! note
|
||||
This cannot reliably detect payloads attached to deleted posts.
|
||||
|
||||
=== "OTP"
|
||||
|
||||
```sh
|
||||
./bin/pleroma_ctl security spoof-uploaded
|
||||
```
|
||||
|
||||
=== "From Source"
|
||||
|
||||
```sh
|
||||
mix pleroma.security spoof-uploaded
|
||||
```
|
||||
|
||||
### Search for counterfeit posts in database
|
||||
|
||||
Scans all notes in the database for signs of being spoofed.
|
||||
|
||||
!!! note
|
||||
Spoofs targeting local accounts can be detected rather reliably
|
||||
(with some restrictions documented in the task’s logs).
|
||||
Counterfeit posts from remote users cannot. A best-effort attempt is made, but
|
||||
a thorough attacker can avoid this and it may yield a small amount of false positives.
|
||||
|
||||
Should you find counterfeit posts of local users, let other admins know so they can delete the too.
|
||||
|
||||
=== "OTP"
|
||||
|
||||
```sh
|
||||
./bin/pleroma_ctl security spoof-inserted
|
||||
```
|
||||
|
||||
=== "From Source"
|
||||
|
||||
```sh
|
||||
mix pleroma.security spoof-inserted
|
||||
```
|
|
@ -45,3 +45,16 @@
|
|||
8. Remove the dependencies that you don't need anymore (see installation guide). Make sure you don't remove packages that are still needed for other software that you have running!
|
||||
|
||||
[¹]: We assume the database name and user are both "akkoma". If not, you can find the correct name in your config files.
|
||||
|
||||
## Docker installations
|
||||
|
||||
If running behind Docker, it is required to run the above commands inside of a running database container.
|
||||
|
||||
### Example
|
||||
Running `docker compose run --rm db pg_dump <...>` will fail and return:
|
||||
```
|
||||
pg_dump: error: connection to server on socket "/run/postgresql/.s.PGSQL.5432" failed: No such file or directory
|
||||
Is the server running locally and accepting connections on that socket?"
|
||||
```
|
||||
However, first starting just the database container with `docker compose up db -d`, and then running `docker compose exec db pg_dump -d akkoma --format=custom -f </your/backup/dir/akkoma.pgdump>` will successfully generate a database dump.
|
||||
Then to make the file accessible on the host system you can run `docker compose cp db:</your/backup/dir/akkoma.pgdump> </your/target/location>` to copy if from the container.
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
If you run akkoma, you may be inclined to collect metrics to ensure your instance is running smoothly,
|
||||
and that there's nothing quietly failing in the background.
|
||||
|
||||
To facilitate this, akkoma exposes prometheus metrics to be scraped.
|
||||
To facilitate this, akkoma exposes a dashboard and prometheus metrics to be scraped.
|
||||
|
||||
## Prometheus
|
||||
|
||||
|
@ -31,3 +31,15 @@ Once you have your token of the form `Bearer $ACCESS_TOKEN`, you can use that in
|
|||
- targets:
|
||||
- example.com
|
||||
```
|
||||
|
||||
## Dashboard
|
||||
|
||||
Administrators can access a live dashboard under `/phoenix/live_dashboard`
|
||||
giving an overview of uptime, software versions, database stats and more.
|
||||
|
||||
The dashboard also includes a variation of the prometheus metrics, however
|
||||
they do not exactly match due to respective limitations of the dashboard
|
||||
and the prometheus exporter.
|
||||
Even more important, the dashboard collects metrics locally in the browser
|
||||
only while the page is open and cannot give a view on their past history.
|
||||
For proper monitoring it is recommended to set up prometheus.
|
||||
|
|
|
@ -104,31 +104,60 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
## Message rewrite facility
|
||||
|
||||
### :mrf
|
||||
* `policies`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
|
||||
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default).
|
||||
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production.
|
||||
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive).
|
||||
* `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
|
||||
* `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links.
|
||||
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed.
|
||||
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections.
|
||||
* `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines.
|
||||
* `Pleroma.Web.ActivityPub.MRF.FollowBotPolicy`: Automatically follows newly discovered users from the specified bot account. Local accounts, locked accounts, and users with "#nobot" in their bio are respected and excluded from being followed.
|
||||
* `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot.
|
||||
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.NormalizeMarkup`: Pass inbound HTML through a scrubber to make sure it doesn't have anything unusual in it. On by default, cannot be turned off.
|
||||
* `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Append a link to a post that quotes another post with the link to the quoted post, to ensure that software that does not understand quotes can have full context. On by default, cannot be turned off.
|
||||
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
|
||||
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
|
||||
* `transparency_obfuscate_domains`: Show domains with `*` in the middle, to censor them if needed. For example, `ridingho.me` will show as `rid*****.me`
|
||||
* `policies`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
|
||||
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default).
|
||||
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production.
|
||||
* `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections.
|
||||
(See [`:mrf_activity_expiration`](#mrf_activity_expiration))
|
||||
* `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot.
|
||||
* `Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy`: Rejects posts from likely spambots by rejecting posts from new users that contain links.
|
||||
* `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
|
||||
* `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines.
|
||||
* `Pleroma.Web.ActivityPub.MRF.HellthreadPolicy`: Blocks messages with too many mentions.
|
||||
(See [`mrf_hellthread`](#mrf_hellthread))
|
||||
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`: Crawls attachments using their MediaProxy URLs so that the MediaProxy cache is primed.
|
||||
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy`: Drops local activities which have no actual content.
|
||||
(e.g. no attachments and only consists of mentions)
|
||||
* `Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy`: Strips content placeholders from posts
|
||||
(such as the dot from mastodon)
|
||||
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy`: Rejects posts of users the server only recently learned about for a while. Great to block spam accounts. (See [`:mrf_reject_newly_created_account_notes`](#mrf_reject_newly_created_account_notes))
|
||||
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See [`:mrf_rejectnonpublic`](#mrf_rejectnonpublic)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See [`:mrf_simple`](#mrf_simple)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy`: Steals all eligible emoji encountered in posts from remote instances
|
||||
(See [`:mrf_steal_emoji`](#mrf_steal_emoji))
|
||||
* `Pleroma.Web.ActivityPub.MRF.SubchainPolicy`: Selectively runs other MRF policies when messages match (See [`:mrf_subchain`](#mrf_subchain)).
|
||||
* `Pleroma.Web.ActivityPub.MRF.TagPolicy`: Applies policies to individual users based on tags, which can be set using pleroma-fe/admin-fe/any other app that supports Pleroma Admin API. For example it allows marking posts from individual users nsfw (sensitive).
|
||||
* `Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy`: Drops all posts except from users specified in a list.
|
||||
(See [`:mrf_user_allowlist`](#mrf_user_allowlist))
|
||||
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
|
||||
|
||||
Additionally the following MRFs will *always* be aplied and cannot be disabled:
|
||||
|
||||
* `Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy`: Strips users limiting who can send them DMs from the recipients of non-eligible DMs
|
||||
* `Pleroma.Web.ActivityPub.MRF.HashtagPolicy`: Depending on a post’s hashtags it can be rejected, get its sensitive flags force-enabled or removed from the global timeline
|
||||
(See [`:mrf_hashtag`](#mrf_hashtag))
|
||||
* `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Append a link to a post that quotes another post with the link to the quoted post, to ensure that software that does not understand quotes can have full context.
|
||||
(See [`:mrf_inline_quote`](#mrf_inline_quote))
|
||||
* `Pleroma.Web.ActivityPub.MRF.NormalizeMarkup`: Pass inbound HTML through a scrubber to make sure it doesn't have anything unusual in it.
|
||||
(See [`:mrf_normalize_markup`](#mrf_normalize_markup))
|
||||
|
||||
|
||||
## Federation
|
||||
### :activitypub
|
||||
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
||||
* `outgoing_blocks`: Whether to federate blocks to other instances
|
||||
* `blockers_visible`: Whether a user can see the posts of users who blocked them
|
||||
* `deny_follow_blocked`: Whether to disallow following an account that has blocked the user in question
|
||||
* `sign_object_fetches`: Sign object fetches with HTTP signatures
|
||||
* `authorized_fetch_mode`: Require HTTP signatures for AP fetches
|
||||
* `max_collection_objects`: The maximum number of objects to fetch from a remote AP collection.
|
||||
|
||||
### MRF policies
|
||||
|
||||
!!! note
|
||||
|
@ -144,6 +173,7 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
* `report_removal`: List of instances to reject reports from and the reason for doing so.
|
||||
* `avatar_removal`: List of instances to strip avatars from and the reason for doing so.
|
||||
* `banner_removal`: List of instances to strip banners from and the reason for doing so.
|
||||
* `background_removal`: List of instances to strip user backgrounds from and the reason for doing so.
|
||||
* `reject_deletes`: List of instances to reject deletions from and the reason for doing so.
|
||||
|
||||
#### :mrf_subchain
|
||||
|
@ -206,7 +236,9 @@ config :pleroma, :mrf_user_allowlist, %{
|
|||
#### :mrf_steal_emoji
|
||||
* `hosts`: List of hosts to steal emojis from
|
||||
* `rejected_shortcodes`: Regex-list of shortcodes to reject
|
||||
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
|
||||
* `size_limit`: File size limit (in bytes), checked before download if possible (and remote server honest),
|
||||
otherwise or again checked before saving emoji to the disk
|
||||
* `download_unknown_size`: whether to download an emoji when the remote server doesn’t report its size in advance
|
||||
|
||||
#### :mrf_activity_expiration
|
||||
|
||||
|
@ -222,14 +254,24 @@ Notes:
|
|||
- The hashtags in the configuration do not have a leading `#`.
|
||||
- This MRF Policy is always enabled, if you want to disable it you have to set empty lists
|
||||
|
||||
### :activitypub
|
||||
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
||||
* `outgoing_blocks`: Whether to federate blocks to other instances
|
||||
* `blockers_visible`: Whether a user can see the posts of users who blocked them
|
||||
* `deny_follow_blocked`: Whether to disallow following an account that has blocked the user in question
|
||||
* `sign_object_fetches`: Sign object fetches with HTTP signatures
|
||||
* `authorized_fetch_mode`: Require HTTP signatures for AP fetches
|
||||
* `max_collection_objects`: The maximum number of objects to fetch from a remote AP collection.
|
||||
#### :mrf_reject_newly_created_account_notes
|
||||
After initially encountering an user, all their posts
|
||||
will be rejected for the configured time (in seconds).
|
||||
Only drops posts. Follows, reposts, etc. are not affected.
|
||||
|
||||
* `age`: Time below which to reject (in seconds)
|
||||
|
||||
An example: (86400 seconds = 24 hours)
|
||||
|
||||
```elixir
|
||||
config :pleroma, :mrf_reject_newly_created_account_notes, age: 86400
|
||||
```
|
||||
|
||||
#### :mrf_inline_quote
|
||||
* `prefix`: what prefix to prepend to quoted URLs
|
||||
|
||||
#### :mrf_normalize_markup
|
||||
* `scrub_policy`: the scrubbing module to use (by default a built-in HTML sanitiser)
|
||||
|
||||
## Pleroma.User
|
||||
|
||||
|
@ -356,7 +398,8 @@ This section describe PWA manifest instance-specific values. Currently this opti
|
|||
## :media_proxy
|
||||
|
||||
* `enabled`: Enables proxying of remote media to the instance’s proxy
|
||||
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
|
||||
* `base_url`: The base URL to access a user-uploaded file.
|
||||
Using a (sub)domain distinct from the instance endpoint is **strongly** recommended.
|
||||
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
|
||||
* `whitelist`: List of hosts with scheme to bypass the mediaproxy (e.g. `https://example.com`)
|
||||
* `invalidation`: options for remove media from cache after delete object:
|
||||
|
@ -557,8 +600,9 @@ the source code is here: [kocaptcha](https://github.com/koto-bank/kocaptcha). Th
|
|||
|
||||
* `uploader`: Which one of the [uploaders](#uploaders) to use.
|
||||
* `filters`: List of [upload filters](#upload-filters) to use.
|
||||
* `link_name`: When enabled Akkoma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe`
|
||||
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to host the media files via another domain or are using a 3rd party S3 provider.
|
||||
* `link_name`: When enabled Akkoma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers
|
||||
* `base_url`: The base URL to access a user-uploaded file; MUST be configured explicitly.
|
||||
Using a (sub)domain distinct from the instance endpoint is **strongly** recommended. A good value might be `https://media.myakkoma.instance/media/`.
|
||||
* `proxy_remote`: If you're using a remote uploader, Akkoma will proxy media requests instead of redirecting to it.
|
||||
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
|
||||
* `filename_display_max_length`: Set max length of a filename to display. 0 = no limit. Default: 30.
|
||||
|
@ -598,20 +642,29 @@ config :ex_aws, :s3,
|
|||
|
||||
### Upload filters
|
||||
|
||||
#### Pleroma.Upload.Filter.Dedupe
|
||||
|
||||
**Always** active; cannot be turned off.
|
||||
Renames files to their hash and prevents duplicate files filling up the disk.
|
||||
No specific configuration.
|
||||
|
||||
#### Pleroma.Upload.Filter.AnonymizeFilename
|
||||
|
||||
This filter replaces the filename (not the path) of an upload. For complete obfuscation, add
|
||||
`Pleroma.Upload.Filter.Dedupe` before AnonymizeFilename.
|
||||
This filter replaces the declared filename (not the path) of an upload.
|
||||
|
||||
* `text`: Text to replace filenames in links. If empty, `{random}.extension` will be used. You can get the original filename extension by using `{extension}`, for example `custom-file-name.{extension}`.
|
||||
|
||||
#### Pleroma.Upload.Filter.Dedupe
|
||||
#### Pleroma.Upload.Filter.Exiftool.StripMetadata
|
||||
|
||||
No specific configuration.
|
||||
This filter strips metadata with Exiftool leaving color profiles and orientation intact.
|
||||
|
||||
#### Pleroma.Upload.Filter.Exiftool
|
||||
* `purge`: List of Exiftool tag names or tag group names to purge
|
||||
* `preserve`: List of Exiftool tag names or tag group names to preserve even if they occur in the purge list
|
||||
|
||||
This filter only strips the GPS and location metadata with Exiftool leaving color profiles and attributes intact.
|
||||
|
||||
#### Pleroma.Upload.Filter.Exiftool.ReadDescription
|
||||
|
||||
This filter reads the ImageDescription and iptc:Caption-Abstract fields with Exiftool so clients can prefill the media description field.
|
||||
|
||||
No specific configuration.
|
||||
|
||||
|
@ -958,6 +1011,15 @@ config :ueberauth, Ueberauth,
|
|||
]
|
||||
```
|
||||
|
||||
You may also need to set up your frontend to use oauth logins. For example, for `akkoma-fe`:
|
||||
|
||||
```elixir
|
||||
config :pleroma, :frontend_configurations,
|
||||
pleroma_fe: %{
|
||||
loginMethod: "token"
|
||||
}
|
||||
```
|
||||
|
||||
## Link parsing
|
||||
|
||||
### :uri_schemes
|
||||
|
|
|
@ -17,6 +17,16 @@ This sets the Akkoma application server to only listen to the localhost interfac
|
|||
|
||||
This sets the `secure` flag on Akkoma’s session cookie. This makes sure, that the cookie is only accepted over encrypted HTTPs connections. This implicitly renames the cookie from `pleroma_key` to `__Host-pleroma-key` which enforces some restrictions. (see [cookie prefixes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Cookie_prefixes))
|
||||
|
||||
### `Pleroma.Upload, :uploader, :base_url`
|
||||
|
||||
> Recommended value: *anything on a different domain than the instance endpoint; e.g. https://media.myinstance.net/*
|
||||
|
||||
Uploads are user controlled and (unless you’re running a true single-user
|
||||
instance) should therefore not be considered trusted. But the domain is used
|
||||
as a pivilege boundary e.g. by HTTP content security policy and ActivityPub.
|
||||
Having uploads on the same domain enabled several past vulnerabilities
|
||||
able to be exploited by malicious users.
|
||||
|
||||
### `:http_security`
|
||||
|
||||
> Recommended value: `true`
|
||||
|
|
|
@ -6,7 +6,16 @@ With the `mediaproxy` function you can use nginx to cache this content, so users
|
|||
|
||||
## Activate it
|
||||
|
||||
* Edit your nginx config and add the following location:
|
||||
* Edit your nginx config and add the following location to your main server block:
|
||||
```
|
||||
location /proxy {
|
||||
return 404;
|
||||
}
|
||||
```
|
||||
|
||||
* Set up a subdomain for the proxy with its nginx config on the same machine
|
||||
*(the latter is not strictly required, but for simplicity we’ll assume so)*
|
||||
* In this subdomain’s server block add
|
||||
```
|
||||
location /proxy {
|
||||
proxy_cache akkoma_media_cache;
|
||||
|
@ -26,9 +35,9 @@ config :pleroma, :media_proxy,
|
|||
enabled: true,
|
||||
proxy_opts: [
|
||||
redirect_on_failure: true
|
||||
]
|
||||
#base_url: "https://cache.akkoma.social"
|
||||
],
|
||||
base_url: "https://cache.akkoma.social"
|
||||
```
|
||||
If you want to use a subdomain to serve the files, uncomment `base_url`, change the url and add a comma after `true` in the previous line.
|
||||
You **really** should use a subdomain to serve proxied files; while we will fix bugs resulting from this, serving arbitrary remote content on your main domain namespace is a significant attack surface.
|
||||
|
||||
* Restart nginx and Akkoma
|
||||
|
|
|
@ -60,7 +60,7 @@ Example of `my-awesome-theme.json` where we add the name "My Awesome Theme"
|
|||
|
||||
### Set as default theme
|
||||
|
||||
Now we can set the new theme as default in the [Pleroma FE configuration](https://docs-fe.akkoma.dev/stable/CONFIGURATION).
|
||||
Now we can set the new theme as default in the [Pleroma FE configuration](https://docs-fe.akkoma.dev/stable/CONFIGURATION/).
|
||||
|
||||
Example of adding the new theme in the back-end config files
|
||||
```elixir
|
||||
|
|
|
@ -35,6 +35,7 @@ Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_si
|
|||
* `media_removal`: Servers in this group will have media stripped from incoming messages.
|
||||
* `avatar_removal`: Avatars from these servers will be stripped from incoming messages.
|
||||
* `banner_removal`: Banner images from these servers will be stripped from incoming messages.
|
||||
* `background_removal`: User background images from these servers will be stripped from incoming messages.
|
||||
* `report_removal`: Servers in this group will have their reports (flags) rejected.
|
||||
* `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields.
|
||||
* `reject_deletes`: Deletion requests will be rejected from these servers.
|
||||
|
@ -61,6 +62,32 @@ config :pleroma, :mrf_simple,
|
|||
|
||||
The effects of MRF policies can be very drastic. It is important to use this functionality carefully. Always try to talk to an admin before writing an MRF policy concerning their instance.
|
||||
|
||||
## Hiding or Obfuscating Policies
|
||||
|
||||
You can opt out of publicly displaying all MRF policies or only hide or obfuscate selected domains.
|
||||
|
||||
To just hide everything set:
|
||||
|
||||
```elixir
|
||||
config :pleroma, :mrf,
|
||||
...
|
||||
transparency: false,
|
||||
```
|
||||
|
||||
To hide or obfuscate only select entries, use:
|
||||
|
||||
```elixir
|
||||
config :pleroma, :mrf,
|
||||
...
|
||||
transparency_obfuscate_domains: ["handholdi.ng", "badword.com"],
|
||||
transparency_exclusions: [{"ghost.club", "even a fragment is too spoopy for humans"}]
|
||||
```
|
||||
|
||||
## More MRF Policies
|
||||
|
||||
See the [documentation cheatsheet](cheatsheet.md)
|
||||
for all available MRF policies and their options.
|
||||
|
||||
## Writing your own MRF Policy
|
||||
|
||||
As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `policies` config setting.
|
||||
|
|
|
@ -25,11 +25,14 @@ Tuning the BEAM requires you provide a config file normally called [vm.args](htt
|
|||
|
||||
`ExecStart=/usr/bin/elixir --erl '-args_file /opt/akkoma/config/vm.args' -S /usr/bin/mix phx.server`
|
||||
|
||||
If using an OTP release, set the `RELEASE_VM_ARGS` environment variable to the path to the vm.args file.
|
||||
|
||||
Check your OS documentation to adopt a similar strategy on other platforms.
|
||||
|
||||
### Virtual Machine and/or few CPU cores
|
||||
|
||||
Disable the busy-waiting. This should generally only be done if you're on a platform that does burst scheduling, like AWS.
|
||||
Disable the busy-waiting. This should generally be done if you're on a platform that does burst scheduling, like AWS, or if you're running other
|
||||
services on the same machine.
|
||||
|
||||
**vm.args:**
|
||||
|
||||
|
@ -39,6 +42,8 @@ Disable the busy-waiting. This should generally only be done if you're on a plat
|
|||
+sbwtdio none
|
||||
```
|
||||
|
||||
These settings are enabled by default for OTP releases
|
||||
|
||||
### Dedicated Hardware
|
||||
|
||||
Enable more busy waiting, increase the internal maximum limit of BEAM processes and ports. You can use this if you run on dedicated hardware, but it is not necessary.
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
# Akkoma API
|
||||
|
||||
Request authentication (if required) and parameters work the same as for [Pleroma API](pleroma_api.md).
|
||||
|
||||
## `/api/v1/akkoma/preferred_frontend/available`
|
||||
### Returns the available frontends which can be picked as the preferred choice
|
||||
* Method: `GET`
|
||||
* Authentication: not required
|
||||
* Params: none
|
||||
* Response: JSON
|
||||
* Example response:
|
||||
```json
|
||||
["pleroma-fe/stable"]
|
||||
```
|
||||
|
||||
!!! note
|
||||
There’s also a browser UI under `/akkoma/frontend`
|
||||
for interactively querying and changing this.
|
||||
|
||||
## `/api/v1/akkoma/preferred_frontend`
|
||||
### Configures the preferred frontend of this session
|
||||
* Method: `PUT`
|
||||
* Authentication: not required
|
||||
* Params:
|
||||
* `frontend_name`: STRING containing one of the available frontends
|
||||
* Response: JSON
|
||||
* Example response:
|
||||
```json
|
||||
{"frontend_name":"pleroma-fe/stable"}
|
||||
```
|
||||
|
||||
!!! note
|
||||
There’s also a browser UI under `/akkoma/frontend`
|
||||
for interactively querying and changing this.
|
||||
|
||||
## `/api/v1/akkoma/metrics`
|
||||
### Provides metrics for Prometheus to scrape
|
||||
* Method: `GET`
|
||||
* Authentication: required (admin:metrics)
|
||||
* Params: none
|
||||
* Response: text
|
||||
* Example response:
|
||||
```
|
||||
# HELP pleroma_remote_users_total
|
||||
# TYPE pleroma_remote_users_total gauge
|
||||
pleroma_remote_users_total 25
|
||||
# HELP pleroma_local_statuses_total
|
||||
# TYPE pleroma_local_statuses_total gauge
|
||||
pleroma_local_statuses_total 17
|
||||
# HELP pleroma_domains_total
|
||||
# TYPE pleroma_domains_total gauge
|
||||
pleroma_domains_total 4
|
||||
# HELP pleroma_local_users_total
|
||||
# TYPE pleroma_local_users_total gauge
|
||||
pleroma_local_users_total 3
|
||||
...
|
||||
```
|
||||
|
||||
## `/api/v1/akkoma/translation/languages`
|
||||
### Returns available source and target languages for automated text translation
|
||||
* Method: `GET`
|
||||
* Authentication: required
|
||||
* Params: none
|
||||
* Response: JSON
|
||||
* Example response:
|
||||
```json
|
||||
{
|
||||
"source": [
|
||||
{"code":"LV", "name":"Latvian"},
|
||||
{"code":"ZH", "name":"Chinese (traditional)"},
|
||||
{"code":"EN-US", "name":"English (American)"}
|
||||
],
|
||||
"target": [
|
||||
{"code":"EN-GB", "name":"English (British)"},
|
||||
{"code":"JP", "name":"Japanese"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## `/api/v1/akkoma/frontend_settings/:frontend_name`
|
||||
### Lists all configuration profiles of the selected frontend for the current user
|
||||
* Method: `GET`
|
||||
* Authentication: required
|
||||
* Params: none
|
||||
* Response: JSON
|
||||
* Example response:
|
||||
```json
|
||||
[
|
||||
{"name":"default","version":31}
|
||||
]
|
||||
```
|
||||
|
||||
## `/api/v1/akkoma/frontend_settings/:frontend_name/:profile_name`
|
||||
### Returns the full selected frontend settings profile of the current user
|
||||
* Method: `GET`
|
||||
* Authentication: required
|
||||
* Params: none
|
||||
* Response: JSON
|
||||
* Example response:
|
||||
```json
|
||||
{
|
||||
"version": 31,
|
||||
"settings": {
|
||||
"streaming": true,
|
||||
"conversationDisplay": "tree",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `/api/v1/akkoma/frontend_settings/:frontend_name/:profile_name`
|
||||
### Updates the frontend settings profile
|
||||
* Method: `PUT`
|
||||
* Authentication: required
|
||||
* Params:
|
||||
* `version`: INTEGER
|
||||
* `settings`: JSON object containing the entire new settings
|
||||
* Response: JSON
|
||||
* Example response:
|
||||
```json
|
||||
{
|
||||
"streaming": false,
|
||||
"conversationDisplay": "tree",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
The `version` field must be increased by exactly one on each update
|
||||
|
||||
## `/api/v1/akkoma/frontend_settings/:frontend_name/:profile_name`
|
||||
### Drops the specified frontend settings profile
|
||||
* Method: `DELETE`
|
||||
* Authentication: required
|
||||
* Params: none
|
||||
* Response: JSON
|
||||
* Example response:
|
||||
```json
|
||||
{"deleted":"ok"}
|
||||
```
|
||||
|
||||
|
||||
## `/api/v1/timelines/bubble`
|
||||
### Returns a timeline for the local and closely related instances
|
||||
Works like all other Mastodon-API timeline queries with the documented
|
||||
[Akkoma-specific additions and tweaks](./differences_in_mastoapi_responses.md#timelines).
|
|
@ -1,6 +1,6 @@
|
|||
# Differences in Mastodon API responses from vanilla Mastodon
|
||||
|
||||
A Akkoma instance can be identified by "<Mastodon version> (compatible; Pleroma <version>)" present in `version` field in response from `/api/v1/instance`
|
||||
A Akkoma instance can be identified by "<Mastodon version> (compatible; Akkoma <version>)" present in `version` field in response from `/api/v1/instance`
|
||||
|
||||
## Flake IDs
|
||||
|
||||
|
@ -8,20 +8,28 @@ Akkoma uses 128-bit ids as opposed to Mastodon's 64 bits. However, just like Mas
|
|||
|
||||
## Timelines
|
||||
|
||||
In addition to Mastodon’s timelines, there is also a “bubble timeline” showing
|
||||
posts from the local instance and a set of closely related instances as chosen
|
||||
by the administrator. It is available under `/api/v1/timelines/bubble`.
|
||||
|
||||
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
|
||||
|
||||
Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`.
|
||||
|
||||
Adding the parameter `reply_visibility` to the public and home timelines queries will filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you.
|
||||
Adding the parameter `reply_visibility` to the public, bubble or home timelines queries will filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you.
|
||||
|
||||
Adding the parameter `instance=lain.com` to the public timeline will show only statuses originating from `lain.com` (or any remote instance).
|
||||
|
||||
Home, public, hashtag & list timelines accept these parameters:
|
||||
All but the direct timeline accept these parameters:
|
||||
|
||||
- `only_media`: show only statuses with media attached
|
||||
- `local`: show only local statuses
|
||||
- `remote`: show only remote statuses
|
||||
|
||||
Home, public, hashtag & list timelines further accept:
|
||||
|
||||
- `local`: show only local statuses
|
||||
|
||||
|
||||
## Statuses
|
||||
|
||||
- `visibility`: has additional possible values `list` and `local` (for local-only statuses)
|
||||
|
@ -113,6 +121,12 @@ Has these additional fields under the `pleroma` object:
|
|||
- `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned.
|
||||
- `favicon`: nullable URL string, Favicon image of the user's instance
|
||||
|
||||
Has these additional fields under the `akkoma` object:
|
||||
|
||||
- `instance`: nullable object with metadata about the user’s instance
|
||||
- `status_ttl_days`: nullable int, default time after which statuses are deleted
|
||||
- `permit_followback`: boolean, whether follows from followed accounts are auto-approved
|
||||
|
||||
### Source
|
||||
|
||||
Has these additional fields under the `pleroma` object:
|
||||
|
|
|
@ -145,47 +145,13 @@ If you want to open your newly installed instance to the world, you should run n
|
|||
doas apk add nginx
|
||||
```
|
||||
|
||||
* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it:
|
||||
|
||||
```shell
|
||||
doas apk add certbot
|
||||
```
|
||||
|
||||
and then set it up:
|
||||
|
||||
```shell
|
||||
doas mkdir -p /var/lib/letsencrypt/
|
||||
doas certbot certonly --email <your@emailaddress> -d <yourdomain> --standalone
|
||||
```
|
||||
|
||||
If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again).
|
||||
|
||||
* Copy the example nginx configuration to the nginx folder
|
||||
|
||||
```shell
|
||||
doas cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/conf.d/akkoma.conf
|
||||
```
|
||||
|
||||
* Before starting nginx edit the configuration and change it to your needs. You must change change `server_name` and the paths to the certificates. You can use `nano` (install with `apk add nano` if missing).
|
||||
|
||||
```
|
||||
server {
|
||||
server_name your.domain;
|
||||
listen 80;
|
||||
...
|
||||
}
|
||||
|
||||
server {
|
||||
server_name your.domain;
|
||||
listen 443 ssl http2;
|
||||
...
|
||||
ssl_trusted_certificate /etc/letsencrypt/live/your.domain/chain.pem;
|
||||
ssl_certificate /etc/letsencrypt/live/your.domain/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/your.domain/privkey.pem;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
* Before starting nginx edit the configuration and change it to your needs. You must change change `server_name`. You can use `nano` (install with `apk add nano` if missing).
|
||||
* Enable and start nginx:
|
||||
|
||||
```shell
|
||||
|
@ -193,10 +159,37 @@ doas rc-update add nginx
|
|||
doas rc-service nginx start
|
||||
```
|
||||
|
||||
If you need to renew the certificate in the future, uncomment the relevant location block in the nginx config and run:
|
||||
* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it:
|
||||
|
||||
```shell
|
||||
doas certbot certonly --email <your@emailaddress> -d <yourdomain> --webroot -w /var/lib/letsencrypt/
|
||||
doas apk add certbot certbot-nginx
|
||||
```
|
||||
|
||||
and then set it up:
|
||||
|
||||
```shell
|
||||
doas mkdir -p /var/lib/letsencrypt/
|
||||
doas certbot --email <your@emailaddress> -d <yourdomain> -d <media_domain> --nginx
|
||||
```
|
||||
|
||||
If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`.
|
||||
|
||||
To automatically renew, set up a cron job like so:
|
||||
|
||||
```shell
|
||||
# Enable the crond service
|
||||
doas rc-update add crond
|
||||
doas rc-service crond start
|
||||
|
||||
# Test that renewals work
|
||||
doas certbot renew --cert-name yourinstance.tld --nginx --dry-run
|
||||
|
||||
# Add the renewal task to cron
|
||||
echo '#!/bin/sh
|
||||
certbot renew --cert-name yourinstance.tld --nginx
|
||||
' | doas tee /etc/periodic/daily/renew-akkoma-cert
|
||||
doas chmod +x /etc/periodic/daily/renew-akkoma-cert
|
||||
|
||||
```
|
||||
|
||||
#### OpenRC service
|
||||
|
|
|
@ -136,16 +136,17 @@ If you want to open your newly installed instance to the world, you should run n
|
|||
sudo pacman -S nginx
|
||||
```
|
||||
|
||||
* Create directories for available and enabled sites:
|
||||
* Copy the example nginx configuration:
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /etc/nginx/sites-{available,enabled}
|
||||
sudo cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/conf.d/akkoma.conf
|
||||
```
|
||||
|
||||
* Append the following line at the end of the `http` block in `/etc/nginx/nginx.conf`:
|
||||
* Before starting nginx edit the configuration and change it to your needs (e.g. change servername, change cert paths)
|
||||
* Enable and start nginx:
|
||||
|
||||
```Nginx
|
||||
include sites-enabled/*;
|
||||
```shell
|
||||
sudo systemctl enable --now nginx.service
|
||||
```
|
||||
|
||||
* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it:
|
||||
|
@ -158,32 +159,18 @@ and then set it up:
|
|||
|
||||
```shell
|
||||
sudo mkdir -p /var/lib/letsencrypt/
|
||||
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --standalone
|
||||
sudo certbot --email <your@emailaddress> -d <yourdomain> -d <media_domain> --nginx
|
||||
```
|
||||
|
||||
If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again).
|
||||
If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`.
|
||||
|
||||
---
|
||||
|
||||
* Copy the example nginx configuration and activate it:
|
||||
To make sure renewals work, enable the appropriate systemd timer:
|
||||
|
||||
```shell
|
||||
sudo cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/sites-available/akkoma.nginx
|
||||
sudo ln -s /etc/nginx/sites-available/akkoma.nginx /etc/nginx/sites-enabled/akkoma.nginx
|
||||
sudo systemctl enable --now certbot-renew.timer
|
||||
```
|
||||
|
||||
* Before starting nginx edit the configuration and change it to your needs (e.g. change servername, change cert paths)
|
||||
* Enable and start nginx:
|
||||
|
||||
```shell
|
||||
sudo systemctl enable --now nginx.service
|
||||
```
|
||||
|
||||
If you need to renew the certificate in the future, uncomment the relevant location block in the nginx config and run:
|
||||
|
||||
```shell
|
||||
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --webroot -w /var/lib/letsencrypt/
|
||||
```
|
||||
Certificate renewal should be handled automatically by Certbot from now on.
|
||||
|
||||
#### Other webserver/proxies
|
||||
|
||||
|
|
|
@ -155,23 +155,6 @@ If you want to open your newly installed instance to the world, you should run n
|
|||
sudo apt install nginx
|
||||
```
|
||||
|
||||
* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it:
|
||||
|
||||
```shell
|
||||
sudo apt install certbot
|
||||
```
|
||||
|
||||
and then set it up:
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /var/lib/letsencrypt/
|
||||
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --standalone
|
||||
```
|
||||
|
||||
If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again).
|
||||
|
||||
---
|
||||
|
||||
* Copy the example nginx configuration and activate it:
|
||||
|
||||
```shell
|
||||
|
@ -186,12 +169,23 @@ sudo ln -s /etc/nginx/sites-available/akkoma.nginx /etc/nginx/sites-enabled/akko
|
|||
sudo systemctl enable --now nginx.service
|
||||
```
|
||||
|
||||
If you need to renew the certificate in the future, uncomment the relevant location block in the nginx config and run:
|
||||
* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it:
|
||||
|
||||
```shell
|
||||
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --webroot -w /var/lib/letsencrypt/
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
```
|
||||
|
||||
and then set it up:
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /var/lib/letsencrypt/
|
||||
sudo certbot --email <your@emailaddress> -d <yourdomain> -d <media_domain> --nginx
|
||||
```
|
||||
|
||||
If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`.
|
||||
|
||||
Certificate renewal should be handled automatically by Certbot from now on.
|
||||
|
||||
#### Other webserver/proxies
|
||||
|
||||
You can find example configurations for them in `/opt/akkoma/installation/`.
|
||||
|
|
|
@ -125,7 +125,26 @@ cp docker-resources/Caddyfile.example docker-resources/Caddyfile
|
|||
|
||||
Then edit the TLD in your caddyfile to the domain you're serving on.
|
||||
|
||||
Uncomment the `caddy` section in the docker compose file,
|
||||
Copy the commented out `caddy` section in `docker-compose.yml` into a new file called `docker-compose.override.yml` like so:
|
||||
```yaml
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
proxy:
|
||||
image: caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
links:
|
||||
- akkoma
|
||||
ports: [
|
||||
"443:443",
|
||||
"80:80"
|
||||
]
|
||||
volumes:
|
||||
- ./docker-resources/Caddyfile:/etc/caddy/Caddyfile
|
||||
- ./caddy-data:/data
|
||||
- ./caddy-config:/config
|
||||
```
|
||||
|
||||
then run `docker compose up -d` again.
|
||||
|
||||
#### Running a reverse proxy on the host
|
||||
|
@ -155,6 +174,12 @@ git pull
|
|||
docker compose restart akkoma db
|
||||
```
|
||||
|
||||
### Modifying the Docker services
|
||||
If you want to modify the services defined in the docker compose file, you can
|
||||
create a new file called `docker-compose.override.yml`. There you can add any
|
||||
overrides or additional services without worrying about git conflicts when a
|
||||
new release comes out.
|
||||
|
||||
#### Further reading
|
||||
|
||||
{! installation/further_reading.include !}
|
||||
|
|
|
@ -135,23 +135,6 @@ If you want to open your newly installed instance to the world, you should run n
|
|||
sudo dnf install nginx
|
||||
```
|
||||
|
||||
* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it:
|
||||
|
||||
```shell
|
||||
sudo dnf install certbot
|
||||
```
|
||||
|
||||
and then set it up:
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /var/lib/letsencrypt/
|
||||
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --standalone
|
||||
```
|
||||
|
||||
If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again).
|
||||
|
||||
---
|
||||
|
||||
* Copy the example nginx configuration and activate it:
|
||||
|
||||
```shell
|
||||
|
@ -165,12 +148,23 @@ sudo cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/conf.d/akkoma.con
|
|||
sudo systemctl enable --now nginx.service
|
||||
```
|
||||
|
||||
If you need to renew the certificate in the future, uncomment the relevant location block in the nginx config and run:
|
||||
* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it:
|
||||
|
||||
```shell
|
||||
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --webroot -w /var/lib/letsencrypt/
|
||||
sudo dnf install certbot python3-certbot-nginx
|
||||
```
|
||||
|
||||
and then set it up:
|
||||
|
||||
```shell
|
||||
sudo certbot --email <your@emailaddress> -d <yourdomain> -d <media_domain> --nginx
|
||||
```
|
||||
|
||||
If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`.
|
||||
|
||||
Certificate renewal should be handled automatically by Certbot from now on.
|
||||
|
||||
|
||||
#### Other webserver/proxies
|
||||
|
||||
You can find example configurations for them in `/opt/akkoma/installation/`.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
## Required dependencies
|
||||
|
||||
* PostgreSQL 9.6+
|
||||
* Elixir 1.14+
|
||||
* Erlang OTP 24+
|
||||
* Elixir 1.14+ (currently tested up to 1.16)
|
||||
* Erlang OTP 25+ (currently tested up to OTP26)
|
||||
* git
|
||||
* file / libmagic
|
||||
* gcc (clang might also work)
|
||||
|
|
|
@ -201,25 +201,6 @@ Assuming you want to open your newly installed federated social network to, well
|
|||
include sites-enabled/*;
|
||||
```
|
||||
|
||||
* Setup your SSL cert, using your method of choice or certbot. If using certbot, install it if you haven't already:
|
||||
|
||||
```shell
|
||||
# emerge --ask app-crypt/certbot app-crypt/certbot-nginx
|
||||
```
|
||||
|
||||
and then set it up:
|
||||
|
||||
```shell
|
||||
# mkdir -p /var/lib/letsencrypt/
|
||||
# certbot certonly --email <your@emailaddress> -d <yourdomain> --standalone
|
||||
```
|
||||
|
||||
If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again). Often the answer to issues with certbot is to use the `--nginx` flag once you have nginx up and running.
|
||||
|
||||
If you are using any additional subdomains, such as for a media proxy, you can re-run the same command with the subdomain in question. When it comes time to renew later, you will not need to run multiple times for each domain, one renew will handle it.
|
||||
|
||||
---
|
||||
|
||||
* Copy the example nginx configuration and activate it:
|
||||
|
||||
```shell
|
||||
|
@ -237,9 +218,24 @@ Pay special attention to the line that begins with `ssl_ecdh_curve`. It is stong
|
|||
|
||||
```shell
|
||||
# rc-update add nginx default
|
||||
# /etc/init.d/nginx start
|
||||
# rc-service nginx start
|
||||
```
|
||||
|
||||
* Setup your SSL cert, using your method of choice or certbot. If using certbot, install it if you haven't already:
|
||||
|
||||
```shell
|
||||
# emerge --ask app-crypt/certbot app-crypt/certbot-nginx
|
||||
```
|
||||
|
||||
and then set it up:
|
||||
|
||||
```shell
|
||||
# mkdir -p /var/lib/letsencrypt/
|
||||
# certbot --email <your@emailaddress> -d <yourdomain> -d <media_domain> --nginx
|
||||
```
|
||||
|
||||
If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`.
|
||||
|
||||
If you are using certbot, it is HIGHLY recommend you set up a cron job that renews your certificate, and that you install the suggested `certbot-nginx` plugin. If you don't do these things, you only have yourself to blame when your instance breaks suddenly because you forgot about it.
|
||||
|
||||
First, ensure that the command you will be installing into your crontab works.
|
||||
|
|
|
@ -21,6 +21,33 @@ fork of Akkoma - luckily this isn't very hard.
|
|||
You'll need to update the backend, then possibly the frontend, depending
|
||||
on your setup.
|
||||
|
||||
## Backup diverging features
|
||||
|
||||
As time goes on Akkoma and Pleroma added or removed different features
|
||||
and reorganised the database in a different way. If you want to be able to
|
||||
migrate back to Pleroma without losing any affected data, you’ll want to
|
||||
make a backup before starting the migration.
|
||||
If you're not interested in migrating back, skip this section
|
||||
*(although it might be a good idea to temporarily keep a full DB backup
|
||||
just in case something unexpected happens during migration)*
|
||||
|
||||
As of 2024-02 you will want to keep a backup of:
|
||||
|
||||
- the entire `chats` and `chat_message_references` tables
|
||||
|
||||
The following columns are not deleted by a migration to Akkoma, but a migration
|
||||
back to Pleroma or future Akkoma upgrades might affect them, so perhaps back them up as well:
|
||||
|
||||
- the `birthday` of users and their `show_birthday` setting
|
||||
- the `expires_at` key of in the `user_relationships` table
|
||||
*(used by temporary mutes)*
|
||||
|
||||
The way cached instance metadata is stored differs, but since those
|
||||
will be refetched and updated anyway, there’s no need for a backup.
|
||||
|
||||
Best check all newer migrations unique to Akkoma/Pleroma
|
||||
to get an up-to-date picture of what needs to be kept.
|
||||
|
||||
## From Source
|
||||
|
||||
If you're running the source Akkoma install, you'll need to set the
|
||||
|
@ -34,16 +61,7 @@ git pull -r
|
|||
# to run "git merge stable" instead (or develop if you want)
|
||||
```
|
||||
|
||||
### WARNING - Migrating from Pleroma Develop
|
||||
If you are on pleroma develop, and have updated since 2022-08, you may have issues with database migrations.
|
||||
|
||||
Please roll back the given migrations:
|
||||
|
||||
```bash
|
||||
MIX_ENV=prod mix ecto.rollback --migrations-path priv/repo/optional_migrations/pleroma_develop_rollbacks -n3
|
||||
```
|
||||
|
||||
Then compile, migrate and restart as usual.
|
||||
And compile as usual.
|
||||
|
||||
## From OTP
|
||||
|
||||
|
@ -53,15 +71,44 @@ This will just be setting the update URL - find your flavour from the [mapping o
|
|||
export FLAVOUR=[the flavour you found above]
|
||||
|
||||
./bin/pleroma_ctl update --zip-url https://akkoma-updates.s3-website.fr-par.scw.cloud/stable/akkoma-$FLAVOUR.zip
|
||||
./bin/pleroma_ctl migrate
|
||||
```
|
||||
|
||||
Then restart. When updating in the future, you canjust use
|
||||
When updating in the future, you can just use
|
||||
|
||||
```bash
|
||||
./bin/pleroma_ctl update --branch stable
|
||||
```
|
||||
|
||||
|
||||
## Database Migrations
|
||||
### WARNING - Migrating from Pleroma past 2022-08
|
||||
If you are on Pleroma stable >= 2.5.0 or Pleroma develop, and
|
||||
have updated since 2022-08, you may have issues with database migrations.
|
||||
|
||||
Please first roll back the given migrations:
|
||||
|
||||
=== "OTP"
|
||||
```bash
|
||||
./bin/pleroma_ctl rollback --migrations-path priv/repo/optional_migrations/pleroma_develop_rollbacks -n5
|
||||
```
|
||||
=== "From Source"
|
||||
```bash
|
||||
MIX_ENV=prod mix ecto.rollback --migrations-path priv/repo/optional_migrations/pleroma_develop_rollbacks -n5
|
||||
```
|
||||
|
||||
### Applying Akkoma Database Migrations
|
||||
|
||||
Just run
|
||||
|
||||
=== "OTP"
|
||||
```bash
|
||||
./bin/pleroma_ctl migrate
|
||||
```
|
||||
=== "From Source"
|
||||
```bash
|
||||
MIX_ENV=prod mix ecto.migrate
|
||||
```
|
||||
|
||||
## Frontend changes
|
||||
|
||||
Akkoma comes with a few frontend changes as well as backend ones,
|
||||
|
@ -130,3 +177,4 @@ MIX_ENV=prod mix ecto.rollback --to 20210416051708
|
|||
```
|
||||
|
||||
Then switch back to Pleroma for updates (similar to how was done to migrate to Akkoma), and remove the front-ends. The front-ends are installed in the `frontends` folder in the [static directory](../configuration/static_dir.md). Once you are back to Pleroma, you will need to run the database migrations again. See the Pleroma documentation for this.
|
||||
After this use your previous backups to restore data from diverging features.
|
||||
|
|
|
@ -14,7 +14,7 @@ Note: the packages are not required with the current default settings of Akkoma.
|
|||
`ImageMagick` is a set of tools to create, edit, compose, or convert bitmap images.
|
||||
|
||||
It is required for the following Akkoma features:
|
||||
* `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Plaroma.Upload/filters` in `config/config.exs`)
|
||||
* `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Pleroma.Upload/filters` in `config/config.exs`)
|
||||
* Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`)
|
||||
|
||||
## `ffmpeg`
|
||||
|
@ -29,4 +29,5 @@ It is required for the following Akkoma features:
|
|||
`exiftool` is media files metadata reader/writer.
|
||||
|
||||
It is required for the following Akkoma features:
|
||||
* `Pleroma.Upload.Filters.Exiftool` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`)
|
||||
* `Pleroma.Upload.Filters.Exiftool.StripMetadata` upload filter (related config: `Pleroma.Upload/filters` in `config/config.exs`)
|
||||
* `Pleroma.Upload.Filters.Exiftool.ReadDescription` upload filter (related config: `Pleroma.Upload/filters` in `config/config.exs`)
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
This guide covers a installation using an OTP release. To install Akkoma from source, please check out the corresponding guide for your distro.
|
||||
|
||||
## Pre-requisites
|
||||
* A machine running Linux with GNU (e.g. Debian, Ubuntu) or musl (e.g. Alpine) libc and an `x86_64` CPU you have root access to. If you are not sure if it's compatible see [Detecting flavour section](#detecting-flavour) below
|
||||
* A machine running Linux with GNU (e.g. Debian, Ubuntu) or musl (e.g. Alpine) libc and an `x86_64` or `arm64` CPU you have root access to. If you are not sure if it's compatible see [Detecting flavour section](#detecting-flavour) below
|
||||
* For installing OTP releases on RedHat-based distros like Fedora and Centos Stream, please follow [this guide](./otp_redhat_en.md) instead.
|
||||
* A (sub)domain pointed to the machine
|
||||
|
||||
You will be running commands as root. If you aren't root already, please elevate your priviledges by executing `sudo su`/`su`.
|
||||
You will be running commands as root. If you aren't root already, please elevate your priviledges by executing `sudo -i`/`su`.
|
||||
|
||||
While in theory OTP releases are possbile to install on any compatible machine, for the sake of simplicity this guide focuses only on Debian/Ubuntu and Alpine.
|
||||
|
||||
|
@ -176,29 +176,24 @@ su akkoma -s $SHELL -lc "./bin/pleroma stop"
|
|||
|
||||
### Setting up nginx and getting Let's Encrypt SSL certificaties
|
||||
|
||||
#### Get a Let's Encrypt certificate
|
||||
```sh
|
||||
certbot certonly --standalone --preferred-challenges http -d yourinstance.tld
|
||||
```
|
||||
|
||||
#### Copy Akkoma nginx configuration to the nginx folder
|
||||
|
||||
The location of nginx configs is dependent on the distro
|
||||
|
||||
=== "Alpine"
|
||||
```
|
||||
cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/conf.d/akkoma.conf
|
||||
cp /opt/akkoma/installation/akkoma.nginx /etc/nginx/conf.d/akkoma.conf
|
||||
```
|
||||
|
||||
=== "Debian/Ubuntu"
|
||||
```
|
||||
cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/sites-available/akkoma.conf
|
||||
cp /opt/akkoma/installation/akkoma.nginx /etc/nginx/sites-available/akkoma.conf
|
||||
ln -s /etc/nginx/sites-available/akkoma.conf /etc/nginx/sites-enabled/akkoma.conf
|
||||
```
|
||||
|
||||
If your distro does not have either of those you can append `include /etc/nginx/akkoma.conf` to the end of the http section in /etc/nginx/nginx.conf and
|
||||
```sh
|
||||
cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/akkoma.conf
|
||||
cp /opt/akkoma/installation/akkoma.nginx /etc/nginx/akkoma.conf
|
||||
```
|
||||
|
||||
#### Edit the nginx config
|
||||
|
@ -209,6 +204,14 @@ $EDITOR path-to-nginx-config
|
|||
# Verify that the config is valid
|
||||
nginx -t
|
||||
```
|
||||
|
||||
#### Get a Let's Encrypt certificate
|
||||
```sh
|
||||
certbot --nginx -d yourinstance.tld -d media.yourinstance.tld
|
||||
```
|
||||
|
||||
If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`.
|
||||
|
||||
#### Start nginx
|
||||
|
||||
=== "Alpine"
|
||||
|
@ -252,32 +255,19 @@ If everything worked, you should see Akkoma-FE when visiting your domain. If tha
|
|||
## Post installation
|
||||
|
||||
### Setting up auto-renew of the Let's Encrypt certificate
|
||||
```sh
|
||||
# Create the directory for webroot challenges
|
||||
mkdir -p /var/lib/letsencrypt
|
||||
|
||||
# Uncomment the webroot method
|
||||
$EDITOR path-to-nginx-config
|
||||
|
||||
# Verify that the config is valid
|
||||
nginx -t
|
||||
```
|
||||
|
||||
=== "Alpine"
|
||||
```
|
||||
# Restart nginx
|
||||
rc-service nginx restart
|
||||
|
||||
# Start the cron daemon and make it start on boot
|
||||
rc-service crond start
|
||||
rc-update add crond
|
||||
|
||||
# Ensure the webroot menthod and post hook is working
|
||||
certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --dry-run --post-hook 'rc-service nginx reload'
|
||||
certbot renew --cert-name yourinstance.tld --nginx --dry-run
|
||||
|
||||
# Add it to the daily cron
|
||||
echo '#!/bin/sh
|
||||
certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --post-hook "rc-service nginx reload"
|
||||
certbot renew --cert-name yourinstance.tld --nginx
|
||||
' > /etc/periodic/daily/renew-akkoma-cert
|
||||
chmod +x /etc/periodic/daily/renew-akkoma-cert
|
||||
|
||||
|
@ -286,22 +276,7 @@ nginx -t
|
|||
```
|
||||
|
||||
=== "Debian/Ubuntu"
|
||||
```
|
||||
# Restart nginx
|
||||
systemctl restart nginx
|
||||
|
||||
# Ensure the webroot menthod and post hook is working
|
||||
certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --dry-run --post-hook 'systemctl reload nginx'
|
||||
|
||||
# Add it to the daily cron
|
||||
echo '#!/bin/sh
|
||||
certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --post-hook "systemctl reload nginx"
|
||||
' > /etc/cron.daily/renew-akkoma-cert
|
||||
chmod +x /etc/cron.daily/renew-akkoma-cert
|
||||
|
||||
# If everything worked the output should contain /etc/cron.daily/renew-akkoma-cert
|
||||
run-parts --test /etc/cron.daily
|
||||
```
|
||||
This should be automatically enabled with the `certbot-renew.timer` systemd unit.
|
||||
|
||||
## Create your first user and set as admin
|
||||
```sh
|
||||
|
|
|
@ -82,6 +82,7 @@ Other than things bundled in the OTP release Akkoma depends on:
|
|||
* PostgreSQL (also utilizes extensions in postgresql-contrib)
|
||||
* nginx (could be swapped with another reverse proxy but this guide covers only it)
|
||||
* certbot (for Let's Encrypt certificates, could be swapped with another ACME client, but this guide covers only it)
|
||||
* If you are using certbot, also install the `python3-certbot-nginx` package for the nginx plugin
|
||||
* libmagic/file
|
||||
|
||||
First, update your system, if not already done:
|
||||
|
@ -169,16 +170,10 @@ sudo -Hu akkoma ./bin/pleroma stop
|
|||
|
||||
### Setting up nginx and getting Let's Encrypt SSL certificaties
|
||||
|
||||
#### Get a Let's Encrypt certificate
|
||||
|
||||
```shell
|
||||
certbot certonly --standalone --preferred-challenges http -d yourinstance.tld
|
||||
```
|
||||
|
||||
#### Copy Akkoma nginx configuration to the nginx folder
|
||||
|
||||
```shell
|
||||
cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/conf.d/akkoma.conf
|
||||
cp /opt/akkoma/installation/akkoma.nginx /etc/nginx/conf.d/akkoma.conf
|
||||
```
|
||||
|
||||
#### Edit the nginx config
|
||||
|
@ -195,8 +190,15 @@ sudo nginx -t
|
|||
sudo systemctl start nginx
|
||||
```
|
||||
|
||||
At this point if you open your (sub)domain in a browser you should see a 502 error, that's because Akkoma is not started yet.
|
||||
#### Get a Let's Encrypt certificate
|
||||
|
||||
```shell
|
||||
sudo certbot --email <your@emailaddress> -d <yourdomain> -d <media_domain> --nginx
|
||||
```
|
||||
|
||||
If that doesn't work the first time, add `--dry-run` to further attempts to avoid being ratelimited as you identify the issue, and do not remove it until the dry run succeeds. A common source of problems are nginx config syntax errors; this can be checked for by running `nginx -t`.
|
||||
|
||||
If you're successful with obtaining the certificates, opening your (sub)domain in a browser will result in a 502 error, since Akkoma hasn't been started yet.
|
||||
|
||||
### Setting up a system service
|
||||
|
||||
|
@ -239,19 +241,11 @@ sudo nginx -t
|
|||
# Restart nginx
|
||||
sudo systemctl restart nginx
|
||||
|
||||
# Ensure the webroot menthod and post hook is working
|
||||
sudo certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --dry-run --post-hook 'systemctl reload nginx'
|
||||
|
||||
# Add it to the daily cron
|
||||
echo '#!/bin/sh
|
||||
certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --post-hook "systemctl reload nginx"
|
||||
' > /etc/cron.daily/renew-akkoma-cert
|
||||
sudo chmod +x /etc/cron.daily/renew-akkoma-cert
|
||||
|
||||
# If everything worked the output should contain /etc/cron.daily/renew-akkoma-cert
|
||||
sudo run-parts --test /etc/cron.daily
|
||||
# Test that renewals work properly
|
||||
sudo certbot renew --cert-name yourinstance.tld --nginx --dry-run
|
||||
```
|
||||
|
||||
Assuming the commands were run successfully, certbot should be able to renew your certificates automatically via the `certbot-renew.timer` systemd unit.
|
||||
|
||||
## Create your first user and set as admin
|
||||
```shell
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
elixir_version=1.14.3
|
||||
erlang_version=25.3
|
|
@ -1,12 +1,9 @@
|
|||
# default nginx site config for Akkoma
|
||||
#
|
||||
# Simple installation instructions:
|
||||
# 1. Install your TLS certificate, possibly using Let's Encrypt.
|
||||
# 2. Replace 'example.tld' with your instance's domain wherever it appears.
|
||||
# 3. Copy this file to /etc/nginx/sites-available/ and then add a symlink to it
|
||||
# in /etc/nginx/sites-enabled/ and run 'nginx -s reload' or restart nginx.
|
||||
# See the documentation at docs.akkoma.dev for your particular distro/OS for
|
||||
# installation instructions.
|
||||
|
||||
proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=10g
|
||||
proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=1g
|
||||
inactive=720m use_temp_path=off;
|
||||
|
||||
# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
|
||||
|
@ -15,25 +12,19 @@ upstream phoenix {
|
|||
server 127.0.0.1:4000 max_fails=5 fail_timeout=60s;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name example.tld;
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
# Uncomment this if you need to use the 'webroot' method with certbot. Make sure
|
||||
# that the directory exists and that it is accessible by the webserver. If you followed
|
||||
# the guide, you already ran 'mkdir -p /var/lib/letsencrypt' to create the folder.
|
||||
# You may need to load this file with the ssl server block commented out, run certbot
|
||||
# to get the certificate, and then uncomment it.
|
||||
#
|
||||
# location ~ /\.well-known/acme-challenge {
|
||||
# root /var/lib/letsencrypt/;
|
||||
# }
|
||||
location / {
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
}
|
||||
# If you are setting up TLS certificates without certbot, uncomment the
|
||||
# following to enable HTTP -> HTTPS redirects. Certbot users don't need to do
|
||||
# this as it will automatically do this for you.
|
||||
# server {
|
||||
# server_name example.tld media.example.tld;
|
||||
#
|
||||
# listen 80;
|
||||
# listen [::]:80;
|
||||
#
|
||||
# location / {
|
||||
# return 301 https://$server_name$request_uri;
|
||||
# }
|
||||
# }
|
||||
|
||||
# Enable SSL session caching for improved performance
|
||||
ssl_session_cache shared:ssl_session_cache:10m;
|
||||
|
@ -41,22 +32,29 @@ ssl_session_cache shared:ssl_session_cache:10m;
|
|||
server {
|
||||
server_name example.tld;
|
||||
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
||||
ssl_session_tickets off;
|
||||
# Once certbot is set up, this will automatically be updated to listen to
|
||||
# port 443 with TLS alongside a redirect from plaintext HTTP.
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem;
|
||||
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
|
||||
# If you are not using Certbot, comment out the above and uncomment/edit the following
|
||||
# listen 443 ssl http2;
|
||||
# listen [::]:443 ssl http2;
|
||||
# ssl_session_timeout 1d;
|
||||
# ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
||||
# ssl_session_tickets off;
|
||||
#
|
||||
# ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem;
|
||||
# ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
|
||||
#
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
|
||||
# ssl_prefer_server_ciphers off;
|
||||
# ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;
|
||||
# ssl_stapling on;
|
||||
# ssl_stapling_verify on;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
|
@ -75,9 +73,43 @@ server {
|
|||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
location ~ ^/(media|proxy) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://phoenix;
|
||||
}
|
||||
}
|
||||
|
||||
# Upload and MediaProxy Subdomain
|
||||
# (see main domain setup for more details)
|
||||
server {
|
||||
server_name media.example.tld;
|
||||
|
||||
# Same as above, will be updated to HTTPS once certbot is set up.
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
# If you are not using certbot, comment the above and copy all the ssl
|
||||
# stuff from above into here.
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/activity+json application/atom+xml;
|
||||
|
||||
# the nginx default is 1m, not enough for large media uploads
|
||||
client_max_body_size 16m;
|
||||
ignore_invalid_headers off;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
location ~ ^/(media|proxy) {
|
||||
proxy_cache akkoma_media_cache;
|
||||
|
@ -91,4 +123,8 @@ server {
|
|||
chunked_transfer_encoding on;
|
||||
proxy_pass http://phoenix;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,6 +130,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
|
|||
}
|
||||
|
||||
File.write!(Path.join(pack_path, "pack.json"), Jason.encode!(pack_json, pretty: true))
|
||||
Pleroma.Emoji.reload()
|
||||
else
|
||||
IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
|
||||
end
|
||||
|
@ -235,6 +236,8 @@ defmodule Mix.Tasks.Pleroma.Emoji do
|
|||
|
||||
IO.puts("#{pack_file} has been created with the #{name} pack")
|
||||
end
|
||||
|
||||
Pleroma.Emoji.reload()
|
||||
end
|
||||
|
||||
def run(["reload"]) do
|
||||
|
|
|
@ -20,6 +20,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
output: :string,
|
||||
output_psql: :string,
|
||||
domain: :string,
|
||||
media_url: :string,
|
||||
instance_name: :string,
|
||||
admin_email: :string,
|
||||
notify_email: :string,
|
||||
|
@ -34,9 +35,9 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
static_dir: :string,
|
||||
listen_ip: :string,
|
||||
listen_port: :string,
|
||||
strip_uploads: :string,
|
||||
anonymize_uploads: :string,
|
||||
dedupe_uploads: :string
|
||||
strip_uploads_metadata: :string,
|
||||
read_uploads_description: :string,
|
||||
anonymize_uploads: :string
|
||||
],
|
||||
aliases: [
|
||||
o: :output,
|
||||
|
@ -64,6 +65,14 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
":"
|
||||
) ++ [443]
|
||||
|
||||
media_url =
|
||||
get_option(
|
||||
options,
|
||||
:media_url,
|
||||
"What base url will uploads use? (e.g https://media.example.com/media)\n" <>
|
||||
" Generally this should NOT use the same domain as the instance "
|
||||
)
|
||||
|
||||
name =
|
||||
get_option(
|
||||
options,
|
||||
|
@ -161,21 +170,38 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
)
|
||||
|> Path.expand()
|
||||
|
||||
{strip_uploads_message, strip_uploads_default} =
|
||||
{strip_uploads_metadata_message, strip_uploads_metadata_default} =
|
||||
if Pleroma.Utils.command_available?("exiftool") do
|
||||
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as installed. (y/n)",
|
||||
{"Do you want to strip metadata from uploaded images? This requires exiftool, it was detected as installed. (y/n)",
|
||||
"y"}
|
||||
else
|
||||
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
|
||||
{"Do you want to strip metadata from uploaded images? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
|
||||
"n"}
|
||||
end
|
||||
|
||||
strip_uploads =
|
||||
strip_uploads_metadata =
|
||||
get_option(
|
||||
options,
|
||||
:strip_uploads,
|
||||
strip_uploads_message,
|
||||
strip_uploads_default
|
||||
:strip_uploads_metadata,
|
||||
strip_uploads_metadata_message,
|
||||
strip_uploads_metadata_default
|
||||
) === "y"
|
||||
|
||||
{read_uploads_description_message, read_uploads_description_default} =
|
||||
if Pleroma.Utils.command_available?("exiftool") do
|
||||
{"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as installed. (y/n)",
|
||||
"y"}
|
||||
else
|
||||
{"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
|
||||
"n"}
|
||||
end
|
||||
|
||||
read_uploads_description =
|
||||
get_option(
|
||||
options,
|
||||
:read_uploads_description,
|
||||
read_uploads_description_message,
|
||||
read_uploads_description_default
|
||||
) === "y"
|
||||
|
||||
anonymize_uploads =
|
||||
|
@ -186,14 +212,6 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
"n"
|
||||
) === "y"
|
||||
|
||||
dedupe_uploads =
|
||||
get_option(
|
||||
options,
|
||||
:dedupe_uploads,
|
||||
"Do you want to deduplicate uploaded files? (y/n)",
|
||||
"n"
|
||||
) === "y"
|
||||
|
||||
Config.put([:instance, :static_dir], static_dir)
|
||||
|
||||
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
|
||||
|
@ -207,6 +225,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
EEx.eval_file(
|
||||
template_dir <> "/sample_config.eex",
|
||||
domain: domain,
|
||||
media_url: media_url,
|
||||
port: port,
|
||||
email: email,
|
||||
notify_email: notify_email,
|
||||
|
@ -229,9 +248,9 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
listen_port: listen_port,
|
||||
upload_filters:
|
||||
upload_filters(%{
|
||||
strip: strip_uploads,
|
||||
anonymize: anonymize_uploads,
|
||||
dedupe: dedupe_uploads
|
||||
strip_metadata: strip_uploads_metadata,
|
||||
read_description: read_uploads_description,
|
||||
anonymize: anonymize_uploads
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -305,11 +324,20 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
end
|
||||
|
||||
defp upload_filters(filters) when is_map(filters) do
|
||||
enabled_filters = []
|
||||
|
||||
enabled_filters =
|
||||
if filters.strip do
|
||||
[Pleroma.Upload.Filter.Exiftool]
|
||||
if filters.read_description do
|
||||
enabled_filters ++ [Pleroma.Upload.Filter.Exiftool.ReadDescription]
|
||||
else
|
||||
[]
|
||||
enabled_filters
|
||||
end
|
||||
|
||||
enabled_filters =
|
||||
if filters.strip_metadata do
|
||||
enabled_filters ++ [Pleroma.Upload.Filter.Exiftool.StripMetadata]
|
||||
else
|
||||
enabled_filters
|
||||
end
|
||||
|
||||
enabled_filters =
|
||||
|
@ -319,13 +347,6 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
enabled_filters
|
||||
end
|
||||
|
||||
enabled_filters =
|
||||
if filters.dedupe do
|
||||
enabled_filters ++ [Pleroma.Upload.Filter.Dedupe]
|
||||
else
|
||||
enabled_filters
|
||||
end
|
||||
|
||||
enabled_filters
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,12 +30,12 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
|
|||
meili_put(
|
||||
"/indexes/objects/settings/ranking-rules",
|
||||
[
|
||||
"published:desc",
|
||||
"words",
|
||||
"exactness",
|
||||
"proximity",
|
||||
"typo",
|
||||
"exactness",
|
||||
"attribute",
|
||||
"published:desc",
|
||||
"sort"
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,330 @@
|
|||
# Akkoma: Magically expressive social media
|
||||
# Copyright © 2024 Akkoma Authors <https://akkoma.dev/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Mix.Tasks.Pleroma.Security do
|
||||
use Mix.Task
|
||||
import Ecto.Query
|
||||
import Mix.Pleroma
|
||||
|
||||
alias Pleroma.Config
|
||||
|
||||
require Logger
|
||||
|
||||
@shortdoc """
|
||||
Security-related tasks, like e.g. checking for signs past exploits were abused.
|
||||
"""
|
||||
|
||||
# Constants etc
|
||||
defp local_id_prefix(), do: Pleroma.Web.Endpoint.url() <> "/"
|
||||
|
||||
defp local_id_pattern(), do: local_id_prefix() <> "%"
|
||||
|
||||
@activity_exts ["activity+json", "activity%2Bjson"]
|
||||
|
||||
defp activity_ext_url_patterns() do
|
||||
for e <- @activity_exts do
|
||||
for suf <- ["", "?%"] do
|
||||
# Escape literal % for use in SQL patterns
|
||||
ee = String.replace(e, "%", "\\%")
|
||||
"%.#{ee}#{suf}"
|
||||
end
|
||||
end
|
||||
|> List.flatten()
|
||||
end
|
||||
|
||||
# Search for malicious uploads exploiting the lack of Content-Type sanitisation from before 2024-03
|
||||
def run(["spoof-uploaded"]) do
|
||||
Logger.put_process_level(self(), :notice)
|
||||
start_pleroma()
|
||||
|
||||
IO.puts("""
|
||||
+------------------------+
|
||||
| SPOOF SEARCH UPLOADS |
|
||||
+------------------------+
|
||||
Checking if any uploads are using privileged types.
|
||||
NOTE if attachment deletion is enabled, payloads used
|
||||
in the past may no longer exist.
|
||||
""")
|
||||
|
||||
do_spoof_uploaded()
|
||||
end
|
||||
|
||||
# Fuzzy search for potentially counterfeit activities in the database resulting from the same exploit
|
||||
def run(["spoof-inserted"]) do
|
||||
Logger.put_process_level(self(), :notice)
|
||||
start_pleroma()
|
||||
|
||||
IO.puts("""
|
||||
+----------------------+
|
||||
| SPOOF SEARCH NOTES |
|
||||
+----------------------+
|
||||
Starting fuzzy search for counterfeit activities.
|
||||
NOTE this can not guarantee detecting all counterfeits
|
||||
and may yield a small percentage of false positives.
|
||||
""")
|
||||
|
||||
do_spoof_inserted()
|
||||
end
|
||||
|
||||
# +-----------------------------+
|
||||
# | S P O O F - U P L O A D E D |
|
||||
# +-----------------------------+
|
||||
defp do_spoof_uploaded() do
|
||||
files =
|
||||
case Config.get!([Pleroma.Upload, :uploader]) do
|
||||
Pleroma.Uploaders.Local ->
|
||||
uploads_search_spoofs_local_dir(Config.get!([Pleroma.Uploaders.Local, :uploads]))
|
||||
|
||||
_ ->
|
||||
IO.puts("""
|
||||
NOTE:
|
||||
Not using local uploader; thus not affected by this exploit.
|
||||
It's impossible to check for files, but in case local uploader was used before
|
||||
or to check if anyone futilely attempted a spoof, notes will still be scanned.
|
||||
""")
|
||||
|
||||
[]
|
||||
end
|
||||
|
||||
emoji = uploads_search_spoofs_local_dir(Config.get!([:instance, :static_dir]))
|
||||
|
||||
post_attachs = uploads_search_spoofs_notes()
|
||||
|
||||
not_orphaned_urls =
|
||||
post_attachs
|
||||
|> Enum.map(fn {_u, _a, url} -> url end)
|
||||
|> MapSet.new()
|
||||
|
||||
orphaned_attachs = upload_search_orphaned_attachments(not_orphaned_urls)
|
||||
|
||||
IO.puts("\nSearch concluded; here are the results:")
|
||||
pretty_print_list_with_title(emoji, "Emoji")
|
||||
pretty_print_list_with_title(files, "Uploaded Files")
|
||||
pretty_print_list_with_title(post_attachs, "(Not Deleted) Post Attachments")
|
||||
pretty_print_list_with_title(orphaned_attachs, "Orphaned Uploads")
|
||||
|
||||
IO.puts("""
|
||||
In total found
|
||||
#{length(emoji)} emoji
|
||||
#{length(files)} uploads
|
||||
#{length(post_attachs)} not deleted posts
|
||||
#{length(orphaned_attachs)} orphaned attachments
|
||||
""")
|
||||
end
|
||||
|
||||
defp uploads_search_spoofs_local_dir(dir) do
|
||||
local_dir = String.replace_suffix(dir, "/", "")
|
||||
|
||||
IO.puts("Searching for suspicious files in #{local_dir}...")
|
||||
|
||||
glob_ext = "{" <> Enum.join(@activity_exts, ",") <> "}"
|
||||
|
||||
Path.wildcard(local_dir <> "/**/*." <> glob_ext, match_dot: true)
|
||||
|> Enum.map(fn path ->
|
||||
String.replace_prefix(path, local_dir <> "/", "")
|
||||
end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
defp uploads_search_spoofs_notes() do
|
||||
IO.puts("Now querying DB for posts with spoofing attachments. This might take a while...")
|
||||
|
||||
patterns = [local_id_pattern() | activity_ext_url_patterns()]
|
||||
|
||||
# if jsonb_array_elemsts in FROM can be used with normal Ecto functions, idk how
|
||||
"""
|
||||
SELECT DISTINCT a.data->>'actor', a.id, url->>'href'
|
||||
FROM public.objects AS o JOIN public.activities AS a
|
||||
ON o.data->>'id' = a.data->>'object',
|
||||
jsonb_array_elements(o.data->'attachment') AS attachs,
|
||||
jsonb_array_elements(attachs->'url') AS url
|
||||
WHERE o.data->>'type' = 'Note' AND
|
||||
o.data->>'id' LIKE $1::text AND (
|
||||
url->>'href' LIKE $2::text OR
|
||||
url->>'href' LIKE $3::text OR
|
||||
url->>'href' LIKE $4::text OR
|
||||
url->>'href' LIKE $5::text
|
||||
)
|
||||
ORDER BY a.data->>'actor', a.id, url->>'href';
|
||||
"""
|
||||
|> Pleroma.Repo.query!(patterns, timeout: :infinity)
|
||||
|> map_raw_id_apid_tuple()
|
||||
end
|
||||
|
||||
defp upload_search_orphaned_attachments(not_orphaned_urls) do
|
||||
IO.puts("""
|
||||
Now querying DB for orphaned spoofing attachment (i.e. their post was deleted,
|
||||
but if :cleanup_attachments was not enabled traces remain in the database)
|
||||
This might take a bit...
|
||||
""")
|
||||
|
||||
patterns = activity_ext_url_patterns()
|
||||
|
||||
"""
|
||||
SELECT DISTINCT attach.id, url->>'href'
|
||||
FROM public.objects AS attach,
|
||||
jsonb_array_elements(attach.data->'url') AS url
|
||||
WHERE (attach.data->>'type' = 'Image' OR
|
||||
attach.data->>'type' = 'Document')
|
||||
AND (
|
||||
url->>'href' LIKE $1::text OR
|
||||
url->>'href' LIKE $2::text OR
|
||||
url->>'href' LIKE $3::text OR
|
||||
url->>'href' LIKE $4::text
|
||||
)
|
||||
ORDER BY attach.id, url->>'href';
|
||||
"""
|
||||
|> Pleroma.Repo.query!(patterns, timeout: :infinity)
|
||||
|> then(fn res -> Enum.map(res.rows, fn [id, url] -> {id, url} end) end)
|
||||
|> Enum.filter(fn {_, url} -> !(url in not_orphaned_urls) end)
|
||||
end
|
||||
|
||||
# +-----------------------------+
|
||||
# | S P O O F - I N S E R T E D |
|
||||
# +-----------------------------+
|
||||
defp do_spoof_inserted() do
|
||||
IO.puts("""
|
||||
Searching for local posts whose Create activity has no ActivityPub id...
|
||||
This is a pretty good indicator, but only for spoofs of local actors
|
||||
and only if the spoofing happened after around late 2021.
|
||||
""")
|
||||
|
||||
idless_create =
|
||||
search_local_notes_without_create_id()
|
||||
|> Enum.sort()
|
||||
|
||||
IO.puts("Done.\n")
|
||||
|
||||
IO.puts("""
|
||||
Now trying to weed out other poorly hidden spoofs.
|
||||
This can't detect all and may have some false positives.
|
||||
""")
|
||||
|
||||
likely_spoofed_posts_set = MapSet.new(idless_create)
|
||||
|
||||
sus_pattern_posts =
|
||||
search_sus_notes_by_id_patterns()
|
||||
|> Enum.filter(fn r -> !(r in likely_spoofed_posts_set) end)
|
||||
|
||||
IO.puts("Done.\n")
|
||||
|
||||
IO.puts("""
|
||||
Finally, searching for spoofed, local user accounts.
|
||||
(It's impossible to detect spoofed remote users)
|
||||
""")
|
||||
|
||||
spoofed_users = search_bogus_local_users()
|
||||
|
||||
pretty_print_list_with_title(sus_pattern_posts, "Maybe Spoofed Posts")
|
||||
pretty_print_list_with_title(idless_create, "Likely Spoofed Posts")
|
||||
pretty_print_list_with_title(spoofed_users, "Spoofed local user accounts")
|
||||
|
||||
IO.puts("""
|
||||
In total found:
|
||||
#{length(spoofed_users)} bogus users
|
||||
#{length(idless_create)} likely spoofed posts
|
||||
#{length(sus_pattern_posts)} maybe spoofed posts
|
||||
""")
|
||||
end
|
||||
|
||||
defp search_local_notes_without_create_id() do
|
||||
Pleroma.Object
|
||||
|> where([o], fragment("?->>'id' LIKE ?", o.data, ^local_id_pattern()))
|
||||
|> join(:inner, [o], a in Pleroma.Activity,
|
||||
on: fragment("?->>'object' = ?->>'id'", a.data, o.data)
|
||||
)
|
||||
|> where([o, a], fragment("NOT (? \\? 'id') OR ?->>'id' IS NULL", a.data, a.data))
|
||||
|> select([o, a], {a.id, fragment("?->>'id'", o.data)})
|
||||
|> order_by([o, a], a.id)
|
||||
|> Pleroma.Repo.all(timeout: :infinity)
|
||||
end
|
||||
|
||||
defp search_sus_notes_by_id_patterns() do
|
||||
[ep1, ep2, ep3, ep4] = activity_ext_url_patterns()
|
||||
|
||||
Pleroma.Object
|
||||
|> where(
|
||||
[o],
|
||||
# for local objects we know exactly how a genuine id looks like
|
||||
# (though a thorough attacker can emulate this)
|
||||
# for remote posts, use some best-effort patterns
|
||||
fragment(
|
||||
"""
|
||||
(?->>'id' LIKE ? AND ?->>'id' NOT SIMILAR TO
|
||||
? || 'objects/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}')
|
||||
""",
|
||||
o.data,
|
||||
^local_id_pattern(),
|
||||
o.data,
|
||||
^local_id_prefix()
|
||||
) or
|
||||
fragment("?->>'id' LIKE ?", o.data, "%/emoji/%") or
|
||||
fragment("?->>'id' LIKE ?", o.data, "%/media/%") or
|
||||
fragment("?->>'id' LIKE ?", o.data, "%/proxy/%") or
|
||||
fragment("?->>'id' LIKE ?", o.data, ^ep1) or
|
||||
fragment("?->>'id' LIKE ?", o.data, ^ep2) or
|
||||
fragment("?->>'id' LIKE ?", o.data, ^ep3) or
|
||||
fragment("?->>'id' LIKE ?", o.data, ^ep4)
|
||||
)
|
||||
|> join(:inner, [o], a in Pleroma.Activity,
|
||||
on: fragment("?->>'object' = ?->>'id'", a.data, o.data)
|
||||
)
|
||||
|> select([o, a], {a.id, fragment("?->>'id'", o.data)})
|
||||
|> order_by([o, a], a.id)
|
||||
|> Pleroma.Repo.all(timeout: :infinity)
|
||||
end
|
||||
|
||||
defp search_bogus_local_users() do
|
||||
Pleroma.User.Query.build(%{})
|
||||
|> where([u], u.local == false and like(u.ap_id, ^local_id_pattern()))
|
||||
|> order_by([u], u.ap_id)
|
||||
|> select([u], u.ap_id)
|
||||
|> Pleroma.Repo.all(timeout: :infinity)
|
||||
end
|
||||
|
||||
# +-----------------------------------+
|
||||
# | module-specific utility functions |
|
||||
# +-----------------------------------+
|
||||
defp pretty_print_list_with_title(list, title) do
|
||||
title_len = String.length(title)
|
||||
title_underline = String.duplicate("=", title_len)
|
||||
IO.puts(title)
|
||||
IO.puts(title_underline)
|
||||
pretty_print_list(list)
|
||||
end
|
||||
|
||||
defp pretty_print_list([]), do: IO.puts("")
|
||||
|
||||
defp pretty_print_list([{a, o} | rest])
|
||||
when (is_binary(a) or is_number(a)) and is_binary(o) do
|
||||
IO.puts(" {#{a}, #{o}}")
|
||||
pretty_print_list(rest)
|
||||
end
|
||||
|
||||
defp pretty_print_list([{u, a, o} | rest])
|
||||
when is_binary(a) and is_binary(u) and is_binary(o) do
|
||||
IO.puts(" {#{u}, #{a}, #{o}}")
|
||||
pretty_print_list(rest)
|
||||
end
|
||||
|
||||
defp pretty_print_list([e | rest]) when is_binary(e) do
|
||||
IO.puts(" #{e}")
|
||||
pretty_print_list(rest)
|
||||
end
|
||||
|
||||
defp pretty_print_list([e | rest]), do: pretty_print_list([inspect(e) | rest])
|
||||
|
||||
defp map_raw_id_apid_tuple(res) do
|
||||
user_prefix = local_id_prefix() <> "users/"
|
||||
|
||||
Enum.map(res.rows, fn
|
||||
[uid, aid, oid] ->
|
||||
{
|
||||
String.replace_prefix(uid, user_prefix, ""),
|
||||
FlakeId.to_string(aid),
|
||||
oid
|
||||
}
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -11,6 +11,7 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
alias Pleroma.UserInviteToken
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
use Pleroma.Web, :verified_routes
|
||||
|
||||
@shortdoc "Manages Pleroma users"
|
||||
@moduledoc File.read!("docs/docs/administration/CLI_tasks/user.md")
|
||||
|
@ -113,11 +114,7 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
|
||||
shell_info("Generated password reset token for #{user.nickname}")
|
||||
|
||||
IO.puts(
|
||||
"URL: #{Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint,
|
||||
:reset,
|
||||
token.token)}"
|
||||
)
|
||||
IO.puts("URL: #{~p[/api/v1/pleroma/password_reset/#{token.token}]}")
|
||||
else
|
||||
_ ->
|
||||
shell_error("No local user #{nickname}")
|
||||
|
@ -303,13 +300,7 @@ defmodule Mix.Tasks.Pleroma.User do
|
|||
{:ok, invite} <- UserInviteToken.create_invite(options) do
|
||||
shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " "))
|
||||
|
||||
url =
|
||||
Pleroma.Web.Router.Helpers.redirect_url(
|
||||
Pleroma.Web.Endpoint,
|
||||
:registration_page,
|
||||
invite.token
|
||||
)
|
||||
|
||||
url = url(~p[/registration/#{invite.token}])
|
||||
IO.puts(url)
|
||||
else
|
||||
error ->
|
||||
|
|
|
@ -26,7 +26,6 @@ defmodule Phoenix.Transports.WebSocket.Raw do
|
|||
conn
|
||||
|> fetch_query_params
|
||||
|> Transport.transport_log(opts[:transport_log])
|
||||
|> Transport.force_ssl(handler, endpoint, opts)
|
||||
|> Transport.check_origin(handler, endpoint, opts)
|
||||
|
||||
case conn do
|
||||
|
|
|
@ -26,6 +26,15 @@ defmodule Pleroma.Activity.Pruner do
|
|||
|> Repo.delete_all(timeout: :infinity)
|
||||
end
|
||||
|
||||
def prune_updates do
|
||||
before_time = cutoff()
|
||||
|
||||
from(a in Activity,
|
||||
where: fragment("?->>'type' = ?", a.data, "Update") and a.inserted_at < ^before_time
|
||||
)
|
||||
|> Repo.delete_all(timeout: :infinity)
|
||||
end
|
||||
|
||||
def prune_removes do
|
||||
before_time = cutoff()
|
||||
|
||||
|
|
|
@ -158,8 +158,9 @@ defmodule Pleroma.Application do
|
|||
|> String.to_existing_atom()
|
||||
|> Code.ensure_loaded!()
|
||||
end)
|
||||
|
||||
# Use this when 1.15 is standard
|
||||
#|> Code.ensure_all_loaded!()
|
||||
# |> Code.ensure_all_loaded!()
|
||||
end
|
||||
|
||||
defp cachex_children do
|
||||
|
@ -287,6 +288,7 @@ defmodule Pleroma.Application do
|
|||
|> Config.get([])
|
||||
|> Pleroma.HTTP.AdapterHelper.add_pool_size(pool_size)
|
||||
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy_pool(proxy)
|
||||
|> Pleroma.HTTP.AdapterHelper.ensure_ipv6()
|
||||
|> Keyword.put(:name, MyFinch)
|
||||
|
||||
[{Finch, config}]
|
||||
|
|
|
@ -164,7 +164,8 @@ defmodule Pleroma.ApplicationRequirements do
|
|||
|
||||
defp check_system_commands!(:ok) do
|
||||
filter_commands_statuses = [
|
||||
check_filter(Pleroma.Upload.Filter.Exiftool, "exiftool"),
|
||||
check_filter(Pleroma.Upload.Filter.Exiftool.StripMetadata, "exiftool"),
|
||||
check_filter(Pleroma.Upload.Filter.Exiftool.ReadDescription, "exiftool"),
|
||||
check_filter(Pleroma.Upload.Filter.Mogrify, "mogrify"),
|
||||
check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"),
|
||||
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "mogrify"),
|
||||
|
|
|
@ -68,7 +68,10 @@ defmodule Akkoma.Collections.Fetcher do
|
|||
items
|
||||
end
|
||||
else
|
||||
{:error, {"Object has been deleted", _, _}} ->
|
||||
{:error, :not_found} ->
|
||||
items
|
||||
|
||||
{:error, :forbidden} ->
|
||||
items
|
||||
|
||||
{:error, error} ->
|
||||
|
|
|
@ -22,6 +22,43 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
"\n* `config :pleroma, :instance, :quarantined_instances` is now covered by `:pleroma, :mrf_simple, :reject`"}
|
||||
]
|
||||
|
||||
def check_exiftool_filter do
|
||||
filters = Config.get([Pleroma.Upload]) |> Keyword.get(:filters, [])
|
||||
|
||||
if Pleroma.Upload.Filter.Exiftool in filters do
|
||||
Logger.warning("""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your config is using Exiftool as a filter instead of Exiftool.StripMetadata. This should work for now, but you are advised to change to the new configuration to prevent possible issues later:
|
||||
|
||||
```
|
||||
config :pleroma, Pleroma.Upload,
|
||||
filters: [Pleroma.Upload.Filter.Exiftool]
|
||||
```
|
||||
|
||||
Is now
|
||||
|
||||
|
||||
```
|
||||
config :pleroma, Pleroma.Upload,
|
||||
filters: [Pleroma.Upload.Filter.Exiftool.StripMetadata]
|
||||
```
|
||||
""")
|
||||
|
||||
new_config =
|
||||
filters
|
||||
|> Enum.map(fn
|
||||
Pleroma.Upload.Filter.Exiftool -> Pleroma.Upload.Filter.Exiftool.StripMetadata
|
||||
filter -> filter
|
||||
end)
|
||||
|
||||
Config.put([Pleroma.Upload, :filters], new_config)
|
||||
|
||||
:error
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def check_simple_policy_tuples do
|
||||
has_strings =
|
||||
Config.get([:mrf_simple])
|
||||
|
@ -182,7 +219,10 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
check_quarantined_instances_tuples(),
|
||||
check_transparency_exclusions_tuples(),
|
||||
check_simple_policy_tuples(),
|
||||
check_http_adapter()
|
||||
check_http_adapter(),
|
||||
check_uploader_base_url_set(),
|
||||
check_uploader_base_url_is_not_base_domain(),
|
||||
check_exiftool_filter()
|
||||
]
|
||||
|> Enum.reduce(:ok, fn
|
||||
:ok, :ok -> :ok
|
||||
|
@ -337,4 +377,54 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
|||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def check_uploader_base_url_set() do
|
||||
uses_local_uploader? = Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.Local
|
||||
base_url = Pleroma.Config.get([Pleroma.Upload, :base_url])
|
||||
|
||||
if base_url || !uses_local_uploader? do
|
||||
:ok
|
||||
else
|
||||
Logger.error("""
|
||||
!!!WARNING!!!
|
||||
Your config does not specify a base_url for uploads!
|
||||
Please make the following change:\n
|
||||
\n* `config :pleroma, Pleroma.Upload, base_url: "https://example.com/media/`
|
||||
\n
|
||||
\nPlease note that it is HEAVILY recommended to use a subdomain to host user-uploaded media!
|
||||
""")
|
||||
|
||||
# This is a hard exit - the uploader will not work without a base_url
|
||||
raise ArgumentError, message: "No base_url set for uploads - please set one in your config!"
|
||||
end
|
||||
end
|
||||
|
||||
def check_uploader_base_url_is_not_base_domain() do
|
||||
uses_local_uploader? = Config.get([Pleroma.Upload, :uploader]) == Pleroma.Uploaders.Local
|
||||
|
||||
uploader_host =
|
||||
[Pleroma.Upload, :base_url]
|
||||
|> Pleroma.Config.get()
|
||||
|> URI.parse()
|
||||
|> Map.get(:host)
|
||||
|
||||
akkoma_host =
|
||||
[Pleroma.Web.Endpoint, :url]
|
||||
|> Pleroma.Config.get()
|
||||
|> Keyword.get(:host)
|
||||
|
||||
if uploader_host == akkoma_host && uses_local_uploader? do
|
||||
Logger.error("""
|
||||
!!!WARNING!!!
|
||||
Your Akkoma Host and your Upload base_url's host are the same!
|
||||
This can potentially be insecure!
|
||||
|
||||
It is HIGHLY recommended that you migrate your media uploads
|
||||
to a subdomain at your earliest convenience
|
||||
""")
|
||||
end
|
||||
|
||||
# This isn't actually an error condition, just a warning
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,7 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do
|
|||
with_runtime_config =
|
||||
if File.exists?(config_path) do
|
||||
# <https://git.pleroma.social/pleroma/pleroma/-/issues/3135>
|
||||
%File.Stat{mode: mode} = File.lstat!(config_path)
|
||||
%File.Stat{mode: mode} = File.stat!(config_path)
|
||||
|
||||
if Bitwise.band(mode, 0o007) > 0 do
|
||||
raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}"
|
||||
|
|
|
@ -110,7 +110,7 @@ defmodule Pleroma.Config.TransferTask do
|
|||
|
||||
defp configure({_, :backends, _, merged}) do
|
||||
# removing current backends
|
||||
Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1)
|
||||
Enum.each(Application.get_env(:logger, :backends, []), &Logger.remove_backend/1)
|
||||
|
||||
Enum.each(merged, &Logger.add_backend/1)
|
||||
|
||||
|
|
|
@ -6,10 +6,13 @@ defmodule Pleroma.Emails.AdminEmail do
|
|||
@moduledoc "Admin emails"
|
||||
|
||||
import Swoosh.Email
|
||||
|
||||
use Pleroma.Web, :mailer
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
use Phoenix.VerifiedRoutes,
|
||||
endpoint: Pleroma.Web.Endpoint,
|
||||
router: Pleroma.Web.Router
|
||||
|
||||
defp instance_config, do: Config.get(:instance)
|
||||
defp instance_name, do: instance_config()[:name]
|
||||
|
@ -45,7 +48,7 @@ defmodule Pleroma.Emails.AdminEmail do
|
|||
statuses
|
||||
|> Enum.map(fn
|
||||
%{id: id} ->
|
||||
status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, id)
|
||||
status_url = url(~p[/notice/#{id}])
|
||||
"<li><a href=\"#{status_url}\">#{status_url}</li>"
|
||||
|
||||
%{"id" => id} when is_binary(id) ->
|
||||
|
|
|
@ -55,12 +55,61 @@ defmodule Pleroma.Emails.Mailer do
|
|||
|
||||
@doc false
|
||||
def validate_dependency do
|
||||
parse_config([])
|
||||
parse_config([], defaults: false)
|
||||
|> Keyword.get(:adapter)
|
||||
|> Swoosh.Mailer.validate_dependency()
|
||||
end
|
||||
|
||||
defp parse_config(config) do
|
||||
Swoosh.Mailer.parse_config(@otp_app, __MODULE__, @mailer_config, config)
|
||||
defp ensure_charlist(input) do
|
||||
case input do
|
||||
i when is_binary(i) -> String.to_charlist(input)
|
||||
i when is_list(i) -> i
|
||||
end
|
||||
end
|
||||
|
||||
defp default_config(adapter, conf, opts)
|
||||
|
||||
defp default_config(_, _, defaults: false) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp default_config(Swoosh.Adapters.SMTP, conf, _) do
|
||||
# gen_smtp and Erlang's tls defaults are very barebones, if nothing is set.
|
||||
# Add sane defaults for our usecase to make config less painful for admins
|
||||
relay = ensure_charlist(Keyword.get(conf, :relay))
|
||||
ssl_disabled = Keyword.get(conf, :ssl) === false
|
||||
os_cacerts = :public_key.cacerts_get()
|
||||
|
||||
common_tls_opts = [
|
||||
cacerts: os_cacerts,
|
||||
versions: [:"tlsv1.2", :"tlsv1.3"],
|
||||
verify: :verify_peer,
|
||||
# some versions have supposedly issues verifying wildcard certs without this
|
||||
server_name_indication: relay,
|
||||
# the default of 10 is too restrictive
|
||||
depth: 32
|
||||
]
|
||||
|
||||
[
|
||||
auth: :always,
|
||||
no_mx_lookups: false,
|
||||
# Direct SSL/TLS
|
||||
# (if ssl was explicitly disabled, we must not pass TLS options to the socket)
|
||||
ssl: true,
|
||||
sockopts: if(ssl_disabled, do: [], else: common_tls_opts),
|
||||
# STARTTLS upgrade (can't be set to :always when already using direct TLS)
|
||||
tls: :if_available,
|
||||
tls_options: common_tls_opts
|
||||
]
|
||||
end
|
||||
|
||||
defp default_config(_, _, _), do: []
|
||||
|
||||
defp parse_config(config, opts \\ []) do
|
||||
conf = Swoosh.Mailer.parse_config(@otp_app, __MODULE__, @mailer_config, config)
|
||||
adapter = Keyword.get(conf, :adapter)
|
||||
|
||||
default_config(adapter, conf, opts)
|
||||
|> Keyword.merge(conf)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,12 +6,11 @@ defmodule Pleroma.Emails.UserEmail do
|
|||
@moduledoc "User emails"
|
||||
|
||||
require Pleroma.Web.Gettext
|
||||
use Pleroma.Web, :mailer
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Gettext
|
||||
alias Pleroma.Web.Router
|
||||
|
||||
import Swoosh.Email
|
||||
import Phoenix.Swoosh, except: [render_body: 3]
|
||||
|
@ -75,7 +74,7 @@ defmodule Pleroma.Emails.UserEmail do
|
|||
|
||||
def password_reset_email(user, token) when is_binary(token) do
|
||||
Gettext.with_locale_or_default user.language do
|
||||
password_reset_url = Router.Helpers.reset_password_url(Endpoint, :reset, token)
|
||||
password_reset_url = url(~p[/api/v1/pleroma/password_reset/#{token}])
|
||||
|
||||
html_body =
|
||||
Gettext.dpgettext(
|
||||
|
@ -108,12 +107,7 @@ defmodule Pleroma.Emails.UserEmail do
|
|||
to_name \\ nil
|
||||
) do
|
||||
Gettext.with_locale_or_default user.language do
|
||||
registration_url =
|
||||
Router.Helpers.redirect_url(
|
||||
Endpoint,
|
||||
:registration_page,
|
||||
user_invite_token.token
|
||||
)
|
||||
registration_url = url(~p[/registration/#{user_invite_token.token}])
|
||||
|
||||
html_body =
|
||||
Gettext.dpgettext(
|
||||
|
@ -146,13 +140,7 @@ defmodule Pleroma.Emails.UserEmail do
|
|||
|
||||
def account_confirmation_email(user) do
|
||||
Gettext.with_locale_or_default user.language do
|
||||
confirmation_url =
|
||||
Router.Helpers.confirm_email_url(
|
||||
Endpoint,
|
||||
:confirm_email,
|
||||
user.id,
|
||||
to_string(user.confirmation_token)
|
||||
)
|
||||
confirmation_url = url(~p[/api/account/confirm_email/#{user.id}/#{user.confirmation_token}])
|
||||
|
||||
html_body =
|
||||
Gettext.dpgettext(
|
||||
|
@ -342,7 +330,7 @@ defmodule Pleroma.Emails.UserEmail do
|
|||
|> Pleroma.JWT.generate_and_sign!()
|
||||
|> Base.encode64()
|
||||
|
||||
Router.Helpers.subscription_url(Endpoint, :unsubscribe, token)
|
||||
url(~p[/mailer/unsubscribe/#{token}])
|
||||
end
|
||||
|
||||
def backup_is_ready_email(backup, admin_user_id \\ nil) do
|
||||
|
|
|
@ -26,12 +26,37 @@ defmodule Pleroma.Emoji.Pack do
|
|||
alias Pleroma.Emoji.Pack
|
||||
alias Pleroma.Utils
|
||||
|
||||
# Invalid/Malicious names are supposed to be filtered out before path joining,
|
||||
# but there are many entrypoints to affected functions so as the code changes
|
||||
# we might accidentally let an unsanitised name slip through.
|
||||
# To make sure, use the below which crash the process otherwise.
|
||||
|
||||
# ALWAYS use this when constructing paths from external name!
|
||||
# (name meaning it must be only a single path component)
|
||||
defp path_join_name_safe(dir, name) do
|
||||
if to_string(name) != Path.basename(name) or name in ["..", ".", ""] do
|
||||
raise "Invalid or malicious pack name: #{name}"
|
||||
else
|
||||
Path.join(dir, name)
|
||||
end
|
||||
end
|
||||
|
||||
# ALWAYS use this to join external paths
|
||||
# (which are allowed to have several components)
|
||||
defp path_join_safe(dir, path) do
|
||||
{:ok, safe_path} = Path.safe_relative(path)
|
||||
Path.join(dir, safe_path)
|
||||
end
|
||||
|
||||
@spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
|
||||
def create(name) do
|
||||
with :ok <- validate_not_empty([name]),
|
||||
dir <- Path.join(emoji_path(), name),
|
||||
dir <- path_join_name_safe(emoji_path(), name),
|
||||
:ok <- File.mkdir(dir) do
|
||||
save_pack(%__MODULE__{pack_file: Path.join(dir, "pack.json")})
|
||||
save_pack(%__MODULE__{
|
||||
path: dir,
|
||||
pack_file: Path.join(dir, "pack.json")
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -65,7 +90,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
|
||||
def delete(name) do
|
||||
with :ok <- validate_not_empty([name]),
|
||||
pack_path <- Path.join(emoji_path(), name) do
|
||||
pack_path <- path_join_name_safe(emoji_path(), name) do
|
||||
File.rm_rf(pack_path)
|
||||
end
|
||||
end
|
||||
|
@ -89,7 +114,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
end)
|
||||
end
|
||||
|
||||
@spec add_file(t(), String.t(), Path.t(), Plug.Upload.t()) ::
|
||||
@spec add_file(t(), String.t(), Path.t(), Plug.Upload.t() | binary()) ::
|
||||
{:ok, t()}
|
||||
| {:error, File.posix() | atom()}
|
||||
def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do
|
||||
|
@ -107,7 +132,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
Enum.map_reduce(emojies, pack, fn item, emoji_pack ->
|
||||
emoji_file = %Plug.Upload{
|
||||
filename: item[:filename],
|
||||
path: Path.join(tmp_dir, item[:path])
|
||||
path: path_join_safe(tmp_dir, item[:path])
|
||||
}
|
||||
|
||||
{:ok, updated_pack} =
|
||||
|
@ -137,6 +162,14 @@ defmodule Pleroma.Emoji.Pack do
|
|||
end
|
||||
|
||||
def add_file(%Pack{} = pack, shortcode, filename, %Plug.Upload{} = file) do
|
||||
try_add_file(pack, shortcode, filename, file)
|
||||
end
|
||||
|
||||
def add_file(%Pack{} = pack, shortcode, filename, filedata) when is_binary(filedata) do
|
||||
try_add_file(pack, shortcode, filename, filedata)
|
||||
end
|
||||
|
||||
defp try_add_file(%Pack{} = pack, shortcode, filename, file) do
|
||||
with :ok <- validate_not_empty([shortcode, filename]),
|
||||
:ok <- validate_emoji_not_exists(shortcode),
|
||||
{:ok, updated_pack} <- do_add_file(pack, shortcode, filename, file) do
|
||||
|
@ -189,6 +222,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
{:ok, results} <- File.ls(emoji_path) do
|
||||
names =
|
||||
results
|
||||
# items come from File.ls, thus safe
|
||||
|> Enum.map(&Path.join(emoji_path, &1))
|
||||
|> Enum.reject(fn path ->
|
||||
File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
|
||||
|
@ -287,8 +321,8 @@ defmodule Pleroma.Emoji.Pack do
|
|||
|
||||
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()}
|
||||
def load_pack(name) do
|
||||
name = Path.basename(name)
|
||||
pack_file = Path.join([emoji_path(), name, "pack.json"])
|
||||
pack_dir = path_join_name_safe(emoji_path(), name)
|
||||
pack_file = Path.join(pack_dir, "pack.json")
|
||||
|
||||
with {:ok, _} <- File.stat(pack_file),
|
||||
{:ok, pack_data} <- File.read(pack_file) do
|
||||
|
@ -412,7 +446,13 @@ defmodule Pleroma.Emoji.Pack do
|
|||
end
|
||||
|
||||
defp create_archive_and_cache(pack, hash) do
|
||||
files = [~c"pack.json" | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)]
|
||||
files = [
|
||||
~c"pack.json"
|
||||
| Enum.map(pack.files, fn {_, file} ->
|
||||
{:ok, file} = Path.safe_relative(file)
|
||||
to_charlist(file)
|
||||
end)
|
||||
]
|
||||
|
||||
{:ok, {_, result}} =
|
||||
:zip.zip(~c"#{pack.name}.zip", files, [:memory, cwd: to_charlist(pack.path)])
|
||||
|
@ -474,7 +514,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
end
|
||||
|
||||
defp save_file(%Plug.Upload{path: upload_path}, pack, filename) do
|
||||
file_path = Path.join(pack.path, filename)
|
||||
file_path = path_join_safe(pack.path, filename)
|
||||
create_subdirs(file_path)
|
||||
|
||||
with {:ok, _} <- File.copy(upload_path, file_path) do
|
||||
|
@ -482,6 +522,12 @@ defmodule Pleroma.Emoji.Pack do
|
|||
end
|
||||
end
|
||||
|
||||
defp save_file(file_data, pack, filename) when is_binary(file_data) do
|
||||
file_path = path_join_safe(pack.path, filename)
|
||||
create_subdirs(file_path)
|
||||
File.write(file_path, file_data, [:binary])
|
||||
end
|
||||
|
||||
defp put_emoji(pack, shortcode, filename) do
|
||||
files = Map.put(pack.files, shortcode, filename)
|
||||
%{pack | files: files, files_count: length(Map.keys(files))}
|
||||
|
@ -493,8 +539,8 @@ defmodule Pleroma.Emoji.Pack do
|
|||
end
|
||||
|
||||
defp rename_file(pack, filename, new_filename) do
|
||||
old_path = Path.join(pack.path, filename)
|
||||
new_path = Path.join(pack.path, new_filename)
|
||||
old_path = path_join_safe(pack.path, filename)
|
||||
new_path = path_join_safe(pack.path, new_filename)
|
||||
create_subdirs(new_path)
|
||||
|
||||
with :ok <- File.rename(old_path, new_path) do
|
||||
|
@ -512,7 +558,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
|
||||
defp remove_file(pack, shortcode) do
|
||||
with {:ok, filename} <- get_filename(pack, shortcode),
|
||||
emoji <- Path.join(pack.path, filename),
|
||||
emoji <- path_join_safe(pack.path, filename),
|
||||
:ok <- File.rm(emoji) do
|
||||
remove_dir_if_empty(emoji, filename)
|
||||
end
|
||||
|
@ -530,7 +576,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
|
||||
defp get_filename(pack, shortcode) do
|
||||
with %{^shortcode => filename} when is_binary(filename) <- pack.files,
|
||||
file_path <- Path.join(pack.path, filename),
|
||||
file_path <- path_join_safe(pack.path, filename),
|
||||
{:ok, _} <- File.stat(file_path) do
|
||||
{:ok, filename}
|
||||
else
|
||||
|
@ -568,7 +614,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
end
|
||||
|
||||
defp copy_as(remote_pack, local_name) do
|
||||
path = Path.join(emoji_path(), local_name)
|
||||
path = path_join_name_safe(emoji_path(), local_name)
|
||||
|
||||
%__MODULE__{
|
||||
name: local_name,
|
||||
|
|
|
@ -65,6 +65,15 @@ defmodule Pleroma.HTTP.AdapterHelper do
|
|||
|> put_in([:pools, :default, :size], pool_size)
|
||||
end
|
||||
|
||||
def ensure_ipv6(opts) do
|
||||
# Default transport opts already enable IPv6, so just ensure they're loaded
|
||||
opts
|
||||
|> maybe_add_pools()
|
||||
|> maybe_add_default_pool()
|
||||
|> maybe_add_conn_opts()
|
||||
|> maybe_add_transport_opts()
|
||||
end
|
||||
|
||||
defp maybe_add_pools(opts) do
|
||||
if Keyword.has_key?(opts, :pools) do
|
||||
opts
|
||||
|
@ -96,11 +105,15 @@ defmodule Pleroma.HTTP.AdapterHelper do
|
|||
defp maybe_add_transport_opts(opts) do
|
||||
transport_opts = get_in(opts, [:pools, :default, :conn_opts, :transport_opts])
|
||||
|
||||
unless is_nil(transport_opts) do
|
||||
opts
|
||||
else
|
||||
put_in(opts, [:pools, :default, :conn_opts, :transport_opts], [])
|
||||
end
|
||||
opts =
|
||||
unless is_nil(transport_opts) do
|
||||
opts
|
||||
else
|
||||
put_in(opts, [:pools, :default, :conn_opts, :transport_opts], [])
|
||||
end
|
||||
|
||||
# IPv6 is disabled and IPv4 enabled by default; ensure we can use both
|
||||
put_in(opts, [:pools, :default, :conn_opts, :transport_opts, :inet6], true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
@ -15,8 +15,19 @@ defmodule Pleroma.JobQueueMonitor do
|
|||
|
||||
@impl true
|
||||
def init(state) do
|
||||
:telemetry.attach("oban-monitor-failure", [:oban, :job, :exception], &handle_event/4, nil)
|
||||
:telemetry.attach("oban-monitor-success", [:oban, :job, :stop], &handle_event/4, nil)
|
||||
:telemetry.attach(
|
||||
"oban-monitor-failure",
|
||||
[:oban, :job, :exception],
|
||||
&Pleroma.JobQueueMonitor.handle_event/4,
|
||||
nil
|
||||
)
|
||||
|
||||
:telemetry.attach(
|
||||
"oban-monitor-success",
|
||||
[:oban, :job, :stop],
|
||||
&Pleroma.JobQueueMonitor.handle_event/4,
|
||||
nil
|
||||
)
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
|
|
@ -178,7 +178,10 @@ defmodule Pleroma.Object do
|
|||
ap_id
|
||||
|
||||
Keyword.get(options, :fetch) ->
|
||||
Fetcher.fetch_object_from_id!(ap_id, options)
|
||||
case Fetcher.fetch_object_from_id(ap_id, options) do
|
||||
{:ok, object} -> object
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
true ->
|
||||
get_cached_by_ap_id(ap_id)
|
||||
|
|
|
@ -11,6 +11,9 @@ defmodule Pleroma.Object.Containment do
|
|||
Object containment is an important step in validating remote objects to prevent
|
||||
spoofing, therefore removal of object containment functions is NOT recommended.
|
||||
"""
|
||||
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
def get_actor(%{"actor" => actor}) when is_binary(actor) do
|
||||
actor
|
||||
end
|
||||
|
@ -47,6 +50,31 @@ defmodule Pleroma.Object.Containment do
|
|||
defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok
|
||||
defp compare_uris(_id_uri, _other_uri), do: :error
|
||||
|
||||
defp compare_uris_exact(uri, uri), do: :ok
|
||||
|
||||
defp compare_uris_exact(%URI{} = id, %URI{} = other),
|
||||
do: compare_uris_exact(URI.to_string(id), URI.to_string(other))
|
||||
|
||||
defp compare_uris_exact(id_uri, other_uri)
|
||||
when is_binary(id_uri) and is_binary(other_uri) do
|
||||
norm_id = String.replace_suffix(id_uri, "/", "")
|
||||
norm_other = String.replace_suffix(other_uri, "/", "")
|
||||
if norm_id == norm_other, do: :ok, else: :error
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks whether an URL to fetch from is from the local server.
|
||||
|
||||
We never want to fetch from ourselves; if it’s not in the database
|
||||
it can’t be authentic and must be a counterfeit.
|
||||
"""
|
||||
def contain_local_fetch(id) do
|
||||
case compare_uris(URI.parse(id), Pleroma.Web.Endpoint.struct_url()) do
|
||||
:ok -> :error
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks that an imported AP object's actor matches the host it came from.
|
||||
"""
|
||||
|
@ -62,8 +90,31 @@ defmodule Pleroma.Object.Containment do
|
|||
def contain_origin(id, %{"attributedTo" => actor} = params),
|
||||
do: contain_origin(id, Map.put(params, "actor", actor))
|
||||
|
||||
def contain_origin(_id, _data), do: :error
|
||||
def contain_origin(_id, _data), do: :ok
|
||||
|
||||
@doc """
|
||||
Check whether the fetch URL (after redirects) exactly (sans tralining slash) matches either
|
||||
the canonical ActivityPub id or the objects url field (for display URLs from *key and Mastodon)
|
||||
|
||||
Since this is meant to be used for fetches, anonymous or transient objects are not accepted here.
|
||||
"""
|
||||
def contain_id_to_fetch(url, %{"id" => id} = data) when is_binary(id) do
|
||||
with {:id, :error} <- {:id, compare_uris_exact(id, url)},
|
||||
# "url" can be a "Link" object and this is checked before full normalisation
|
||||
display_url <- Transmogrifier.fix_url(data)["url"],
|
||||
true <- display_url != nil do
|
||||
compare_uris_exact(display_url, url)
|
||||
else
|
||||
{:id, :ok} -> :ok
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def contain_id_to_fetch(_url, _data), do: :error
|
||||
|
||||
@doc """
|
||||
Check whether the object id is from the same host as another id
|
||||
"""
|
||||
def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(other_id) do
|
||||
id_uri = URI.parse(id)
|
||||
other_uri = URI.parse(other_id)
|
||||
|
@ -85,4 +136,12 @@ defmodule Pleroma.Object.Containment do
|
|||
do: contain_origin(id, object)
|
||||
|
||||
def contain_child(_), do: :ok
|
||||
|
||||
@doc "Checks whether two URIs belong to the same domain"
|
||||
def same_origin(id1, id2) do
|
||||
uri1 = URI.parse(id1)
|
||||
uri2 = URI.parse(id2)
|
||||
|
||||
compare_uris(uri1, uri2)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,6 +18,16 @@ defmodule Pleroma.Object.Fetcher do
|
|||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
@moduledoc """
|
||||
This module deals with correctly fetching Acitivity Pub objects in a safe way.
|
||||
|
||||
The core function is `fetch_and_contain_remote_object_from_id/1` which performs
|
||||
the actual fetch and common safety and authenticity checks. Other `fetch_*`
|
||||
function use the former and perform some additional tasks
|
||||
"""
|
||||
|
||||
@mix_env Mix.env()
|
||||
|
||||
defp touch_changeset(changeset) do
|
||||
updated_at =
|
||||
NaiveDateTime.utc_now()
|
||||
|
@ -103,54 +113,78 @@ defmodule Pleroma.Object.Fetcher do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Assumes object already is in our database and refetches from remote to update (e.g. for polls)"
|
||||
def refetch_object(%Object{data: %{"id" => id}} = object) do
|
||||
with {:local, false} <- {:local, Object.local?(object)},
|
||||
{:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
|
||||
{:id, true} <- {:id, new_data["id"] == id},
|
||||
{:ok, object} <- reinject_object(object, new_data) do
|
||||
{:ok, object}
|
||||
else
|
||||
{:local, true} -> {:ok, object}
|
||||
{:id, false} -> {:error, :id_mismatch}
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
# Note: will create a Create activity, which we need internally at the moment.
|
||||
@doc """
|
||||
Fetches a new object and puts it through the processing pipeline for inbound objects
|
||||
|
||||
Note: will also insert a fake Create activity, since atm we internally
|
||||
need everything to be traced back to a Create activity.
|
||||
"""
|
||||
def fetch_object_from_id(id, options \\ []) do
|
||||
with %URI{} = uri <- URI.parse(id),
|
||||
# let's check the URI is even vaguely valid first
|
||||
{:scheme, true} <- {:scheme, uri.scheme == "http" or uri.scheme == "https"},
|
||||
{:valid_uri_scheme, true} <-
|
||||
{:valid_uri_scheme, uri.scheme == "http" or uri.scheme == "https"},
|
||||
# If we have instance restrictions, apply them here to prevent fetching from unwanted instances
|
||||
{:ok, nil} <- Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_reject(uri),
|
||||
{:ok, _} <- Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri),
|
||||
{:mrf_reject_check, {:ok, nil}} <-
|
||||
{:mrf_reject_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_reject(uri)},
|
||||
{:mrf_accept_check, {:ok, _}} <-
|
||||
{:mrf_accept_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri)},
|
||||
{_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
|
||||
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
|
||||
{_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
|
||||
{_, nil} <- {:normalize, Object.normalize(data, fetch: false)},
|
||||
params <- prepare_activity_params(data),
|
||||
{_, :ok} <- {:containment, Containment.contain_origin(id, params)},
|
||||
{_, {:ok, activity}} <-
|
||||
{:transmogrifier, Transmogrifier.handle_incoming(params, options)},
|
||||
{_, _data, %Object{} = object} <-
|
||||
{:object, data, Object.normalize(activity, fetch: false)} do
|
||||
{:ok, object}
|
||||
else
|
||||
{:allowed_depth, false} ->
|
||||
{:error, "Max thread distance exceeded."}
|
||||
{:allowed_depth, false} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:error, :allowed_depth}
|
||||
|
||||
{:scheme, false} ->
|
||||
{:error, "URI Scheme Invalid"}
|
||||
{:valid_uri_scheme, _} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:error, :invalid_uri_scheme}
|
||||
|
||||
{:containment, _} ->
|
||||
{:error, "Object containment failed."}
|
||||
{:mrf_reject_check, _} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:reject, :mrf}
|
||||
|
||||
{:transmogrifier, {:error, {:reject, e}}} ->
|
||||
{:reject, e}
|
||||
{:mrf_accept_check, _} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:reject, :mrf}
|
||||
|
||||
{:transmogrifier, {:reject, e}} ->
|
||||
{:reject, e}
|
||||
{:containment, reason} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:error, reason}
|
||||
|
||||
{:transmogrifier, _} = e ->
|
||||
{:error, e}
|
||||
{:transmogrifier, {:error, {:reject, reason}}} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:reject, reason}
|
||||
|
||||
{:transmogrifier, {:reject, reason}} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:reject, reason}
|
||||
|
||||
{:transmogrifier, reason} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:error, reason}
|
||||
|
||||
{:object, data, nil} ->
|
||||
reinject_object(%Object{}, data)
|
||||
|
@ -161,17 +195,21 @@ defmodule Pleroma.Object.Fetcher do
|
|||
{:fetch_object, %Object{} = object} ->
|
||||
{:ok, object}
|
||||
|
||||
{:fetch, {:error, error}} ->
|
||||
{:error, error}
|
||||
|
||||
{:reject, reason} ->
|
||||
{:reject, reason}
|
||||
{:fetch, {:error, reason}} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:error, reason}
|
||||
|
||||
e ->
|
||||
e
|
||||
log_fetch_error(id, e)
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
defp log_fetch_error(id, error) do
|
||||
Logger.metadata(object: id)
|
||||
Logger.error("Object rejected while fetching #{id} #{inspect(error)}")
|
||||
end
|
||||
|
||||
defp prepare_activity_params(data) do
|
||||
%{
|
||||
"type" => "Create",
|
||||
|
@ -185,26 +223,6 @@ defmodule Pleroma.Object.Fetcher do
|
|||
|> Maps.put_if_present("bcc", data["bcc"])
|
||||
end
|
||||
|
||||
def fetch_object_from_id!(id, options \\ []) do
|
||||
with {:ok, object} <- fetch_object_from_id(id, options) do
|
||||
object
|
||||
else
|
||||
{:error, %Tesla.Mock.Error{}} ->
|
||||
nil
|
||||
|
||||
{:error, {"Object has been deleted", _id, _code}} ->
|
||||
nil
|
||||
|
||||
{:reject, reason} ->
|
||||
Logger.debug("Rejected #{id} while fetching: #{inspect(reason)}")
|
||||
nil
|
||||
|
||||
e ->
|
||||
Logger.error("Error while fetching #{id}: #{inspect(e)}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp make_signature(id, date) do
|
||||
uri = URI.parse(id)
|
||||
|
||||
|
@ -235,6 +253,7 @@ defmodule Pleroma.Object.Fetcher do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Fetches arbitrary remote object and performs basic safety and authenticity checks"
|
||||
def fetch_and_contain_remote_object_from_id(id)
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(%{"id" => id}),
|
||||
|
@ -243,18 +262,46 @@ defmodule Pleroma.Object.Fetcher do
|
|||
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
||||
Logger.debug("Fetching object #{id} via AP")
|
||||
|
||||
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
|
||||
{:ok, body} <- get_object(id),
|
||||
with {:valid_uri_scheme, true} <- {:valid_uri_scheme, String.starts_with?(id, "http")},
|
||||
%URI{} = uri <- URI.parse(id),
|
||||
{:mrf_reject_check, {:ok, nil}} <-
|
||||
{:mrf_reject_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_reject(uri)},
|
||||
{:mrf_accept_check, {:ok, _}} <-
|
||||
{:mrf_accept_check, Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri)},
|
||||
{:local_fetch, :ok} <- {:local_fetch, Containment.contain_local_fetch(id)},
|
||||
{:ok, final_id, body} <- get_object(id),
|
||||
{:ok, data} <- safe_json_decode(body),
|
||||
:ok <- Containment.contain_origin_from_id(id, data) do
|
||||
unless Instances.reachable?(id) do
|
||||
Instances.set_reachable(id)
|
||||
{_, :ok} <- {:strict_id, Containment.contain_id_to_fetch(final_id, data)},
|
||||
{_, :ok} <- {:containment, Containment.contain_origin(final_id, data)} do
|
||||
unless Instances.reachable?(final_id) do
|
||||
Instances.set_reachable(final_id)
|
||||
end
|
||||
|
||||
{:ok, data}
|
||||
else
|
||||
{:scheme, _} ->
|
||||
{:error, "Unsupported URI scheme"}
|
||||
{:strict_id, _} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:error, :id_mismatch}
|
||||
|
||||
{:mrf_reject_check, _} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:reject, :mrf}
|
||||
|
||||
{:mrf_accept_check, _} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:reject, :mrf}
|
||||
|
||||
{:valid_uri_scheme, _} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:error, :invalid_uri_scheme}
|
||||
|
||||
{:local_fetch, _} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:error, :local_resource}
|
||||
|
||||
{:containment, reason} ->
|
||||
log_fetch_error(id, reason)
|
||||
{:error, reason}
|
||||
|
||||
{:error, e} ->
|
||||
{:error, e}
|
||||
|
@ -265,47 +312,85 @@ defmodule Pleroma.Object.Fetcher do
|
|||
end
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(_id),
|
||||
do: {:error, "id must be a string"}
|
||||
do: {:error, :invalid_id}
|
||||
|
||||
defp check_crossdomain_redirect(final_host, original_url)
|
||||
|
||||
# HOPEFULLY TEMPORARY
|
||||
# Basically none of our Tesla mocks in tests set the (supposed to
|
||||
# exist for Tesla proper) url parameter for their responses
|
||||
# causing almost every fetch in test to fail otherwise
|
||||
if @mix_env == :test do
|
||||
defp check_crossdomain_redirect(nil, _) do
|
||||
{:cross_domain_redirect, false}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_crossdomain_redirect(final_host, original_url) do
|
||||
{:cross_domain_redirect, final_host != URI.parse(original_url).host}
|
||||
end
|
||||
|
||||
if @mix_env == :test do
|
||||
defp get_final_id(nil, initial_url), do: initial_url
|
||||
defp get_final_id("", initial_url), do: initial_url
|
||||
end
|
||||
|
||||
defp get_final_id(final_url, _intial_url) do
|
||||
final_url
|
||||
end
|
||||
|
||||
@doc "Do NOT use; only public for use in tests"
|
||||
def get_object(id) do
|
||||
date = Pleroma.Signature.signed_date()
|
||||
|
||||
headers =
|
||||
[{"accept", "application/activity+json"}]
|
||||
[
|
||||
# The first is required by spec, the second provided as a fallback for buggy implementations
|
||||
{"accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""},
|
||||
{"accept", "application/activity+json"}
|
||||
]
|
||||
|> maybe_date_fetch(date)
|
||||
|> sign_fetch(id, date)
|
||||
|
||||
case HTTP.get(id, headers) do
|
||||
{:ok, %{body: body, status: code, headers: headers}} when code in 200..299 ->
|
||||
case List.keyfind(headers, "content-type", 0) do
|
||||
{_, content_type} ->
|
||||
case Plug.Conn.Utils.media_type(content_type) do
|
||||
{:ok, "application", "activity+json", _} ->
|
||||
{:ok, body}
|
||||
with {:ok, %{body: body, status: code, headers: headers, url: final_url}}
|
||||
when code in 200..299 <-
|
||||
HTTP.get(id, headers),
|
||||
remote_host <-
|
||||
URI.parse(final_url).host,
|
||||
{:cross_domain_redirect, false} <-
|
||||
check_crossdomain_redirect(remote_host, id),
|
||||
{:has_content_type, {_, content_type}} <-
|
||||
{:has_content_type, List.keyfind(headers, "content-type", 0)},
|
||||
{:parse_content_type, {:ok, "application", subtype, type_params}} <-
|
||||
{:parse_content_type, Plug.Conn.Utils.media_type(content_type)} do
|
||||
final_id = get_final_id(final_url, id)
|
||||
|
||||
{:ok, "application", "ld+json",
|
||||
%{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
|
||||
{:ok, body}
|
||||
case {subtype, type_params} do
|
||||
{"activity+json", _} ->
|
||||
{:ok, final_id, body}
|
||||
|
||||
# pixelfed sometimes (and only sometimes) responds with http instead of https
|
||||
{:ok, "application", "ld+json",
|
||||
%{"profile" => "http://www.w3.org/ns/activitystreams"}} ->
|
||||
{:ok, body}
|
||||
{"ld+json", %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
|
||||
{:ok, final_id, body}
|
||||
|
||||
_ ->
|
||||
{:error, {:content_type, content_type}}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, {:content_type, nil}}
|
||||
end
|
||||
_ ->
|
||||
{:error, {:content_type, content_type}}
|
||||
end
|
||||
else
|
||||
{:ok, %{status: code}} when code in [401, 403] ->
|
||||
{:error, :forbidden}
|
||||
|
||||
{:ok, %{status: code}} when code in [404, 410] ->
|
||||
{:error, {"Object has been deleted", id, code}}
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, e} ->
|
||||
{:error, e}
|
||||
|
||||
{:has_content_type, _} ->
|
||||
{:error, {:content_type, nil}}
|
||||
|
||||
{:parse_content_type, e} ->
|
||||
{:error, {:content_type, e}}
|
||||
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
|
|
|
@ -17,6 +17,8 @@ defmodule Pleroma.ReverseProxy do
|
|||
@failed_request_ttl :timer.seconds(60)
|
||||
@methods ~w(GET HEAD)
|
||||
|
||||
@allowed_mime_types Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types], [])
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
||||
def max_read_duration_default, do: @max_read_duration
|
||||
|
@ -61,11 +63,14 @@ defmodule Pleroma.ReverseProxy do
|
|||
|
||||
"""
|
||||
@inline_content_types [
|
||||
"image/avif",
|
||||
"image/gif",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/jxl",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"image/webp",
|
||||
"audio/mpeg",
|
||||
"audio/mp3",
|
||||
"video/webm",
|
||||
|
@ -250,6 +255,7 @@ defmodule Pleroma.ReverseProxy do
|
|||
headers
|
||||
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|
||||
|> build_resp_cache_headers(opts)
|
||||
|> sanitise_content_type()
|
||||
|> build_resp_content_disposition_header(opts)
|
||||
|> build_csp_headers()
|
||||
|> Keyword.merge(Keyword.get(opts, :resp_headers, []))
|
||||
|
@ -279,6 +285,21 @@ defmodule Pleroma.ReverseProxy do
|
|||
end
|
||||
end
|
||||
|
||||
defp sanitise_content_type(headers) do
|
||||
original_ct = get_content_type(headers)
|
||||
|
||||
safe_ct =
|
||||
Pleroma.Web.Plugs.Utils.get_safe_mime_type(
|
||||
%{allowed_mime_types: @allowed_mime_types},
|
||||
original_ct
|
||||
)
|
||||
|
||||
[
|
||||
{"content-type", safe_ct}
|
||||
| Enum.filter(headers, fn {k, _v} -> k != "content-type" end)
|
||||
]
|
||||
end
|
||||
|
||||
defp build_resp_content_disposition_header(headers, opts) do
|
||||
opt = Keyword.get(opts, :inline_content_types, @inline_content_types)
|
||||
|
||||
|
|
|
@ -76,6 +76,6 @@ defmodule Pleroma.Signature do
|
|||
def signed_date, do: signed_date(NaiveDateTime.utc_now())
|
||||
|
||||
def signed_date(%NaiveDateTime{} = date) do
|
||||
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
|
||||
Timex.lformat!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT", "en")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -61,12 +61,23 @@ defmodule Pleroma.Upload do
|
|||
width: integer(),
|
||||
height: integer(),
|
||||
blurhash: String.t(),
|
||||
description: String.t(),
|
||||
path: String.t()
|
||||
}
|
||||
|
||||
@always_enabled_filters [Pleroma.Upload.Filter.AnonymizeFilename]
|
||||
@always_enabled_filters [Pleroma.Upload.Filter.Dedupe]
|
||||
|
||||
defstruct [:id, :name, :tempfile, :content_type, :width, :height, :blurhash, :path]
|
||||
defstruct [
|
||||
:id,
|
||||
:name,
|
||||
:tempfile,
|
||||
:content_type,
|
||||
:width,
|
||||
:height,
|
||||
:blurhash,
|
||||
:description,
|
||||
:path
|
||||
]
|
||||
|
||||
@spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
|
||||
@doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct."
|
||||
|
@ -76,7 +87,7 @@ defmodule Pleroma.Upload do
|
|||
with {:ok, upload} <- prepare_upload(upload, opts),
|
||||
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
|
||||
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
|
||||
description = Map.get(opts, :description) || "",
|
||||
description = Map.get(upload, :description) || "",
|
||||
{_, true} <-
|
||||
{:description_limit,
|
||||
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
|
||||
|
@ -152,7 +163,8 @@ defmodule Pleroma.Upload do
|
|||
id: UUID.generate(),
|
||||
name: file.filename,
|
||||
tempfile: file.path,
|
||||
content_type: file.content_type
|
||||
content_type: file.content_type,
|
||||
description: opts.description
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
@ -172,7 +184,8 @@ defmodule Pleroma.Upload do
|
|||
id: UUID.generate(),
|
||||
name: hash <> "." <> ext,
|
||||
tempfile: tmp_path,
|
||||
content_type: content_type
|
||||
content_type: content_type,
|
||||
description: opts.description
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
@ -235,7 +248,7 @@ defmodule Pleroma.Upload do
|
|||
|
||||
case uploader do
|
||||
Pleroma.Uploaders.Local ->
|
||||
upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/"
|
||||
upload_base_url
|
||||
|
||||
Pleroma.Uploaders.S3 ->
|
||||
bucket = Config.get([Pleroma.Uploaders.S3, :bucket])
|
||||
|
@ -261,7 +274,7 @@ defmodule Pleroma.Upload do
|
|||
end
|
||||
|
||||
_ ->
|
||||
public_endpoint || upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/"
|
||||
public_endpoint || upload_base_url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do
|
||||
@moduledoc """
|
||||
Gets a valid description from the related EXIF tags and provides them in the response if no description is provided yet.
|
||||
It will first check ImageDescription, when that doesn't probide a valid description, it will check iptc:Caption-Abstract.
|
||||
A valid description means the fields are filled in and not too long (see `:instance, :description_limit`).
|
||||
"""
|
||||
@behaviour Pleroma.Upload.Filter
|
||||
|
||||
@spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()}
|
||||
|
||||
def filter(%Pleroma.Upload{description: description})
|
||||
when is_binary(description),
|
||||
do: {:ok, :noop}
|
||||
|
||||
def filter(%Pleroma.Upload{tempfile: file} = upload),
|
||||
do: {:ok, :filtered, upload |> Map.put(:description, read_description_from_exif_data(file))}
|
||||
|
||||
def filter(_, _), do: {:ok, :noop}
|
||||
|
||||
defp read_description_from_exif_data(file) do
|
||||
nil
|
||||
|> read_when_empty(file, "-ImageDescription")
|
||||
|> read_when_empty(file, "-iptc:Caption-Abstract")
|
||||
end
|
||||
|
||||
defp read_when_empty(current_description, _, _) when is_binary(current_description),
|
||||
do: current_description
|
||||
|
||||
defp read_when_empty(_, file, tag) do
|
||||
try do
|
||||
{tag_content, 0} =
|
||||
System.cmd("exiftool", ["-b", "-s3", tag, file],
|
||||
stderr_to_stdout: true,
|
||||
parallelism: true
|
||||
)
|
||||
|
||||
tag_content = String.trim(tag_content)
|
||||
|
||||
if tag_content != "" and
|
||||
String.length(tag_content) <=
|
||||
Pleroma.Config.get([:instance, :description_limit]),
|
||||
do: tag_content,
|
||||
else: nil
|
||||
rescue
|
||||
_ in ErlangError -> nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,24 +2,42 @@
|
|||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.Exiftool do
|
||||
defmodule Pleroma.Upload.Filter.Exiftool.StripMetadata do
|
||||
@moduledoc """
|
||||
Strips GPS related EXIF tags and overwrites the file in place.
|
||||
Tries to strip all image metadata but colorspace and orientation overwriting the file in place.
|
||||
Also strips or replaces filesystem metadata e.g., timestamps.
|
||||
"""
|
||||
@behaviour Pleroma.Upload.Filter
|
||||
|
||||
alias Pleroma.Config
|
||||
|
||||
@purge_default ["all", "CommonIFD0"]
|
||||
@preserve_default ["ColorSpaceTags", "Orientation"]
|
||||
|
||||
@spec filter(Pleroma.Upload.t()) :: {:ok, :noop} | {:ok, :filtered} | {:error, String.t()}
|
||||
|
||||
# Formats not compatible with exiftool at this time
|
||||
def filter(%Pleroma.Upload{content_type: "image/heic"}), do: {:ok, :noop}
|
||||
def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop}
|
||||
def filter(%Pleroma.Upload{content_type: "image/svg+xml"}), do: {:ok, :noop}
|
||||
def filter(%Pleroma.Upload{content_type: "image/jxl"}), do: {:ok, :noop}
|
||||
|
||||
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
|
||||
purge_args =
|
||||
Config.get([__MODULE__, :purge], @purge_default)
|
||||
|> Enum.map(fn mgroup -> "-" <> mgroup <> "=" end)
|
||||
|
||||
preserve_args =
|
||||
Config.get([__MODULE__, :preserve], @preserve_default)
|
||||
|> Enum.map(fn mgroup -> "-" <> mgroup end)
|
||||
|> then(fn
|
||||
# If -TagsFromFile is not followed by tag selectors, it will copy most available tags
|
||||
[] -> []
|
||||
args -> ["-TagsFromFile", "@" | args]
|
||||
end)
|
||||
|
||||
args = ["-ignoreMinorErrors", "-overwrite_original" | purge_args] ++ preserve_args ++ [file]
|
||||
|
||||
try do
|
||||
case System.cmd("exiftool", ["-overwrite_original", "-gps:all=", file], parallelism: true) do
|
||||
case System.cmd("exiftool", args, parallelism: true) do
|
||||
{_response, 0} -> {:ok, :filtered}
|
||||
{error, 1} -> {:error, error}
|
||||
end
|
|
@ -44,6 +44,8 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Web.RelMe
|
||||
alias Pleroma.Workers.BackgroundWorker
|
||||
|
||||
use Pleroma.Web, :verified_routes
|
||||
|
||||
require Logger
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
@ -158,6 +160,7 @@ defmodule Pleroma.User do
|
|||
field(:last_status_at, :naive_datetime)
|
||||
field(:language, :string)
|
||||
field(:status_ttl_days, :integer, default: nil)
|
||||
field(:permit_followback, :boolean, default: false)
|
||||
|
||||
field(:accepts_direct_messages_from, Ecto.Enum,
|
||||
values: [:everybody, :people_i_follow, :nobody],
|
||||
|
@ -379,6 +382,10 @@ defmodule Pleroma.User do
|
|||
do_optional_url(user.banner, "#{Endpoint.url()}/images/banner.png", options)
|
||||
end
|
||||
|
||||
def background_url(user) do
|
||||
do_optional_url(user.background, nil, no_default: true)
|
||||
end
|
||||
|
||||
defp do_optional_url(field, default, options) do
|
||||
case field do
|
||||
%{"url" => [%{"href" => href} | _]} when is_binary(href) ->
|
||||
|
@ -463,6 +470,7 @@ defmodule Pleroma.User do
|
|||
:avatar,
|
||||
:ap_enabled,
|
||||
:banner,
|
||||
:background,
|
||||
:is_locked,
|
||||
:last_refreshed_at,
|
||||
:uri,
|
||||
|
@ -542,6 +550,7 @@ defmodule Pleroma.User do
|
|||
:actor_type,
|
||||
:disclose_client,
|
||||
:status_ttl_days,
|
||||
:permit_followback,
|
||||
:accepts_direct_messages_from
|
||||
]
|
||||
)
|
||||
|
@ -960,26 +969,32 @@ defmodule Pleroma.User do
|
|||
|
||||
defp maybe_send_registration_email(_), do: {:ok, :noop}
|
||||
|
||||
def needs_update?(%User{local: true}), do: false
|
||||
def needs_update?(user, options \\ [])
|
||||
def needs_update?(%User{local: true}, _options), do: false
|
||||
def needs_update?(%User{local: false, last_refreshed_at: nil}, _options), do: true
|
||||
|
||||
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
|
||||
|
||||
def needs_update?(%User{local: false} = user) do
|
||||
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
|
||||
def needs_update?(%User{local: false} = user, options) do
|
||||
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >=
|
||||
Keyword.get(options, :maximum_age, 86_400)
|
||||
end
|
||||
|
||||
def needs_update?(_), do: true
|
||||
def needs_update?(_, _options), do: true
|
||||
|
||||
# "Locked" (self-locked) users demand explicit authorization of follow requests
|
||||
@spec can_direct_follow_local(User.t(), User.t()) :: true | false
|
||||
def can_direct_follow_local(%User{} = follower, %User{local: true} = followed) do
|
||||
!followed.is_locked || (followed.permit_followback and is_friend_of(follower, followed))
|
||||
end
|
||||
|
||||
@spec maybe_direct_follow(User.t(), User.t()) ::
|
||||
{:ok, User.t(), User.t()} | {:error, String.t()}
|
||||
|
||||
# "Locked" (self-locked) users demand explicit authorization of follow requests
|
||||
def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
|
||||
follow(follower, followed, :follow_pending)
|
||||
end
|
||||
|
||||
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
|
||||
follow(follower, followed)
|
||||
if can_direct_follow_local(follower, followed) do
|
||||
follow(follower, followed)
|
||||
else
|
||||
follow(follower, followed, :follow_pending)
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_direct_follow(%User{} = follower, %User{} = followed) do
|
||||
|
@ -1329,6 +1344,13 @@ defmodule Pleroma.User do
|
|||
|> Repo.all()
|
||||
end
|
||||
|
||||
def is_friend_of(%User{} = potential_friend, %User{local: true} = user) do
|
||||
user
|
||||
|> get_friends_query()
|
||||
|> where(id: ^potential_friend.id)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
def increase_note_count(%User{} = user) do
|
||||
User
|
||||
|> where(id: ^user.id)
|
||||
|
@ -1603,9 +1625,13 @@ defmodule Pleroma.User do
|
|||
def blocks_user?(_, _), do: false
|
||||
|
||||
def blocks_domain?(%User{} = user, %User{} = target) do
|
||||
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
|
||||
%{host: host} = URI.parse(target.ap_id)
|
||||
Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
|
||||
Enum.member?(user.domain_blocks, host)
|
||||
# TODO: functionality should probably be changed such that subdomains block as well,
|
||||
# but as it stands, this just hecks up the relationships endpoint
|
||||
# domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
|
||||
# %{host: host} = URI.parse(target.ap_id)
|
||||
# Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
|
||||
end
|
||||
|
||||
def blocks_domain?(_, _), do: false
|
||||
|
@ -1955,10 +1981,10 @@ defmodule Pleroma.User do
|
|||
|
||||
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
|
||||
|
||||
def get_or_fetch_by_ap_id(ap_id) do
|
||||
def get_or_fetch_by_ap_id(ap_id, options \\ []) do
|
||||
cached_user = get_cached_by_ap_id(ap_id)
|
||||
|
||||
maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
|
||||
maybe_fetched_user = needs_update?(cached_user, options) && fetch_by_ap_id(ap_id)
|
||||
|
||||
case {cached_user, maybe_fetched_user} do
|
||||
{_, {:ok, %User{} = user}} ->
|
||||
|
@ -2447,12 +2473,7 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
if is_url(raw_value) do
|
||||
frontend_url =
|
||||
Pleroma.Web.Router.Helpers.redirect_url(
|
||||
Pleroma.Web.Endpoint,
|
||||
:redirector_with_meta,
|
||||
nickname
|
||||
)
|
||||
frontend_url = url(~p[/#{nickname}])
|
||||
|
||||
possible_urls = [ap_id, frontend_url]
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ defmodule Pleroma.Web do
|
|||
alias Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Web.Plugs.PlugHelper
|
||||
require Pleroma.Constants
|
||||
|
||||
def controller do
|
||||
quote do
|
||||
|
@ -37,7 +38,7 @@ defmodule Pleroma.Web do
|
|||
import Pleroma.Web.Gettext
|
||||
import Pleroma.Web.TranslationHelpers
|
||||
|
||||
alias Pleroma.Web.Router.Helpers, as: Routes
|
||||
unquote(verified_routes())
|
||||
|
||||
plug(:set_put_layout)
|
||||
|
||||
|
@ -184,7 +185,10 @@ defmodule Pleroma.Web do
|
|||
|
||||
# Import convenience functions from controllers
|
||||
import Phoenix.Controller,
|
||||
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
|
||||
only: [view_module: 1, view_template: 1]
|
||||
|
||||
import Phoenix.Flash
|
||||
alias Phoenix.Flash
|
||||
|
||||
# Include shared imports and aliases for views
|
||||
unquote(view_helpers())
|
||||
|
@ -218,7 +222,7 @@ defmodule Pleroma.Web do
|
|||
|
||||
def router do
|
||||
quote do
|
||||
use Phoenix.Router
|
||||
use Phoenix.Router, helpers: false
|
||||
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
|
@ -246,7 +250,24 @@ defmodule Pleroma.Web do
|
|||
|
||||
import Pleroma.Web.ErrorHelpers
|
||||
import Pleroma.Web.Gettext
|
||||
alias Pleroma.Web.Router.Helpers, as: Routes
|
||||
unquote(verified_routes())
|
||||
end
|
||||
end
|
||||
|
||||
def static_paths, do: Pleroma.Constants.static_only_files()
|
||||
|
||||
def verified_routes do
|
||||
quote do
|
||||
use Phoenix.VerifiedRoutes,
|
||||
endpoint: Pleroma.Web.Endpoint,
|
||||
router: Pleroma.Web.Router,
|
||||
statics: Pleroma.Web.static_paths()
|
||||
end
|
||||
end
|
||||
|
||||
def mailer do
|
||||
quote do
|
||||
unquote(verified_routes())
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
alias Pleroma.Upload
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.UserValidator
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.Streamer
|
||||
alias Pleroma.Web.WebFinger
|
||||
|
@ -1603,6 +1604,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
uri: get_actor_url(data["url"]),
|
||||
ap_enabled: true,
|
||||
banner: normalize_image(data["image"]),
|
||||
background: normalize_image(data["backgroundUrl"]),
|
||||
fields: fields,
|
||||
emoji: emojis,
|
||||
is_locked: is_locked,
|
||||
|
@ -1703,9 +1705,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
Fetcher.fetch_and_contain_remote_object_from_id(first) do
|
||||
{:ok, false}
|
||||
else
|
||||
{:error, {:ok, %{status: code}}} when code in [401, 403] -> {:ok, true}
|
||||
{:error, _} = e -> e
|
||||
e -> {:error, e}
|
||||
{:error, _} -> {:ok, true}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1721,6 +1721,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do
|
||||
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
|
||||
{:valid, {:ok, _, _}} <- {:valid, UserValidator.validate(data, [])},
|
||||
{:ok, data} <- user_data_from_user_object(data, additional) do
|
||||
{:ok, maybe_update_follow_information(data)}
|
||||
else
|
||||
|
@ -1729,10 +1730,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
{:error, {:reject, reason} = e} ->
|
||||
{:reject, reason} = e ->
|
||||
Logger.debug("Rejected user #{ap_id}: #{inspect(reason)}")
|
||||
{:error, e}
|
||||
|
||||
{:valid, reason} ->
|
||||
Logger.debug("Data is not a valid user #{ap_id}: #{inspect(reason)}")
|
||||
{:error, "Not a user"}
|
||||
|
||||
{:error, e} ->
|
||||
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
@ -1792,6 +1797,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end)
|
||||
end
|
||||
|
||||
def pin_data_from_featured_collection(obj) do
|
||||
Logger.error("Could not parse featured collection #{inspect(obj)}")
|
||||
%{}
|
||||
end
|
||||
|
||||
def fetch_and_prepare_featured_from_ap_id(nil) do
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
@ -1828,6 +1838,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
|
||||
|
||||
user =
|
||||
if data.ap_id != ap_id do
|
||||
User.get_cached_by_ap_id(data.ap_id)
|
||||
else
|
||||
user
|
||||
end
|
||||
|
||||
if user do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|
|
|
@ -12,9 +12,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.UserView
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
|
@ -40,11 +38,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
# Note: :following and :followers must be served even without authentication (as via :api)
|
||||
plug(
|
||||
EnsureAuthenticatedPlug
|
||||
when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
|
||||
when action in [:read_inbox]
|
||||
)
|
||||
|
||||
plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
|
||||
|
||||
plug(
|
||||
Pleroma.Web.Plugs.Cache,
|
||||
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
|
||||
|
@ -160,7 +156,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
end
|
||||
|
||||
# GET /relay/following
|
||||
@doc """
|
||||
GET /relay/following
|
||||
"""
|
||||
def relay_following(conn, _params) do
|
||||
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
||||
conn
|
||||
|
@ -197,7 +195,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
end
|
||||
|
||||
# GET /relay/followers
|
||||
@doc """
|
||||
GET /relay/followers
|
||||
"""
|
||||
def relay_followers(conn, _params) do
|
||||
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
||||
conn
|
||||
|
@ -317,14 +317,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|> represent_service_actor(conn)
|
||||
end
|
||||
|
||||
@doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
|
||||
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(UserView)
|
||||
|> render("user.json", %{user: user})
|
||||
end
|
||||
|
||||
def read_inbox(
|
||||
%{assigns: %{user: %User{nickname: nickname} = user}} = conn,
|
||||
%{"nickname" => nickname, "page" => page?} = params
|
||||
|
@ -375,105 +367,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|> json(err)
|
||||
end
|
||||
|
||||
defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
|
||||
when is_map(object) do
|
||||
length =
|
||||
[object["content"], object["summary"], object["name"]]
|
||||
|> Enum.filter(&is_binary(&1))
|
||||
|> Enum.join("")
|
||||
|> String.length()
|
||||
|
||||
limit = Pleroma.Config.get([:instance, :limit])
|
||||
|
||||
if length < limit do
|
||||
object =
|
||||
object
|
||||
|> Transmogrifier.strip_internal_fields()
|
||||
|> Map.put("attributedTo", actor)
|
||||
|> Map.put("actor", actor)
|
||||
|> Map.put("id", Utils.generate_object_id())
|
||||
|
||||
{:ok, Map.put(activity, "object", object)}
|
||||
else
|
||||
{:error,
|
||||
dgettext(
|
||||
"errors",
|
||||
"Character limit (%{limit} characters) exceeded, contains %{length} characters",
|
||||
limit: limit,
|
||||
length: length
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
defp fix_user_message(
|
||||
%User{ap_id: actor} = user,
|
||||
%{"type" => "Delete", "object" => object} = activity
|
||||
) do
|
||||
with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
|
||||
{_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:normalize, _} ->
|
||||
{:error, "No such object found"}
|
||||
|
||||
{:permission, _} ->
|
||||
{:forbidden, "You can't delete this object"}
|
||||
end
|
||||
end
|
||||
|
||||
defp fix_user_message(%User{}, activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
def update_outbox(
|
||||
%{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
|
||||
%{"nickname" => nickname} = params
|
||||
) do
|
||||
params =
|
||||
params
|
||||
|> Map.drop(["nickname"])
|
||||
|> Map.put("id", Utils.generate_activity_id())
|
||||
|> Map.put("actor", actor)
|
||||
|
||||
with {:ok, params} <- fix_user_message(user, params),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
|
||||
%Activity{data: activity_data} <- Activity.normalize(activity) do
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> put_resp_header("location", activity_data["id"])
|
||||
|> json(activity_data)
|
||||
else
|
||||
{:forbidden, message} ->
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(message)
|
||||
|
||||
{:error, message} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(message)
|
||||
|
||||
e ->
|
||||
Logger.warning(fn -> "AP C2S: #{inspect(e)}" end)
|
||||
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json("Bad Request")
|
||||
end
|
||||
end
|
||||
|
||||
def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
|
||||
err =
|
||||
dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
|
||||
nickname: nickname,
|
||||
as_nickname: user.nickname
|
||||
)
|
||||
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(err)
|
||||
end
|
||||
|
||||
defp errors(conn, {:error, :not_found}) do
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|
@ -495,21 +388,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
conn
|
||||
end
|
||||
|
||||
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
|
||||
with {:ok, object} <-
|
||||
ActivityPub.upload(
|
||||
file,
|
||||
actor: User.ap_id(user),
|
||||
description: Map.get(data, "description")
|
||||
) do
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(object.data)
|
||||
end
|
||||
end
|
||||
|
||||
def pinned(conn, %{"nickname" => nickname}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||
conn
|
||||
|
|
|
@ -18,6 +18,8 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
alias Pleroma.Web.CommonAPI.ActivityDraft
|
||||
alias Pleroma.Web.Endpoint
|
||||
|
||||
use Pleroma.Web, :verified_routes
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
def accept_or_reject(actor, activity, type) do
|
||||
|
@ -402,6 +404,6 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
end
|
||||
|
||||
defp pinned_url(nickname) when is_binary(nickname) do
|
||||
Pleroma.Web.Router.Helpers.activity_pub_url(Pleroma.Web.Endpoint, :pinned, nickname)
|
||||
url(~p[/users/#{nickname}/collections/featured])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,14 +6,29 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
|
|||
@moduledoc "Force a quote line into the message content."
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Object
|
||||
|
||||
defp build_inline_quote(prefix, url) do
|
||||
"<span class=\"quote-inline\"><br/><br/>#{prefix}: <a href=\"#{url}\">#{url}</a></span>"
|
||||
end
|
||||
|
||||
defp has_inline_quote?(content, quote_url) do
|
||||
defp resolve_urls(quote_url) do
|
||||
# Fetching here can cause infinite recursion as we run this logic on inbound objects too
|
||||
# This is probably not a problem - its an exceptional corner case for a local user to quote
|
||||
# a post which doesn't exist
|
||||
with %Object{} = obj <- Object.normalize(quote_url, fetch: false) do
|
||||
id = obj.data["id"]
|
||||
url = Map.get(obj.data, "url", id)
|
||||
{id, url, [id, url, quote_url]}
|
||||
else
|
||||
_ -> {quote_url, quote_url, [quote_url]}
|
||||
end
|
||||
end
|
||||
|
||||
defp has_inline_quote?(content, urls) do
|
||||
cond do
|
||||
# Does the quote URL exist in the content?
|
||||
content =~ quote_url -> true
|
||||
Enum.any?(urls, fn url -> content =~ url end) -> true
|
||||
# Does the content already have a .quote-inline span?
|
||||
content =~ "<span class=\"quote-inline\">" -> true
|
||||
# No inline quote found
|
||||
|
@ -22,18 +37,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
|
|||
end
|
||||
|
||||
defp filter_object(%{"quoteUri" => quote_url} = object) do
|
||||
{id, preferred_url, all_urls} = resolve_urls(quote_url)
|
||||
object = Map.put(object, "quoteUri", id)
|
||||
|
||||
content = object["content"] || ""
|
||||
|
||||
if has_inline_quote?(content, quote_url) do
|
||||
if has_inline_quote?(content, all_urls) do
|
||||
object
|
||||
else
|
||||
prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix])
|
||||
|
||||
content =
|
||||
if String.ends_with?(content, "</p>") do
|
||||
String.trim_trailing(content, "</p>") <> build_inline_quote(prefix, quote_url) <> "</p>"
|
||||
String.trim_trailing(content, "</p>") <>
|
||||
build_inline_quote(prefix, preferred_url) <> "</p>"
|
||||
else
|
||||
content <> build_inline_quote(prefix, quote_url)
|
||||
content <> build_inline_quote(prefix, preferred_url)
|
||||
end
|
||||
|
||||
Map.put(object, "content", content)
|
||||
|
|
|
@ -178,6 +178,23 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
defp check_banner_removal(_actor_info, object), do: {:ok, object}
|
||||
|
||||
defp check_background_removal(
|
||||
%{host: actor_host} = _actor_info,
|
||||
%{"backgroundUrl" => _bg} = object
|
||||
) do
|
||||
background_removal =
|
||||
instance_list(:background_removal)
|
||||
|> MRF.subdomains_regex()
|
||||
|
||||
if MRF.subdomain_match?(background_removal, actor_host) do
|
||||
{:ok, Map.delete(object, "backgroundUrl")}
|
||||
else
|
||||
{:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_background_removal(_actor_info, object), do: {:ok, object}
|
||||
|
||||
defp extract_context_uri(%{"conversation" => "tag:" <> rest}) do
|
||||
rest
|
||||
|> String.split(",", parts: 2, trim: true)
|
||||
|
@ -283,7 +300,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
with {:ok, _} <- check_accept(actor_info),
|
||||
{:ok, _} <- check_reject(actor_info),
|
||||
{:ok, object} <- check_avatar_removal(actor_info, object),
|
||||
{:ok, object} <- check_banner_removal(actor_info, object) do
|
||||
{:ok, object} <- check_banner_removal(actor_info, object),
|
||||
{:ok, object} <- check_background_removal(actor_info, object) do
|
||||
{:ok, object}
|
||||
else
|
||||
{:reject, nil} -> {:reject, "[SimplePolicy]"}
|
||||
|
@ -314,6 +332,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
def filter(object), do: {:ok, object}
|
||||
|
||||
defp obfuscate(string) when is_binary(string) do
|
||||
# Want to strip at least two neighbouring chars
|
||||
# to ensure at least one non-dot char is in the obfuscation area
|
||||
stripped = String.length(string) - 6
|
||||
|
||||
{keepstart, keepend} =
|
||||
if stripped > 1 do
|
||||
{3, 3}
|
||||
else
|
||||
{
|
||||
2 - div(1 - stripped, 2),
|
||||
2 + div(stripped, 2)
|
||||
}
|
||||
end
|
||||
|
||||
string
|
||||
|> to_charlist()
|
||||
|> Enum.with_index()
|
||||
|
@ -322,7 +354,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
?.
|
||||
|
||||
{char, index} ->
|
||||
if 3 <= index && index < String.length(string) - 3, do: ?*, else: char
|
||||
if keepstart <= index && index < String.length(string) - keepend, do: ?*, else: char
|
||||
end)
|
||||
|> to_string()
|
||||
end
|
||||
|
@ -433,6 +465,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
key: :banner_removal,
|
||||
description: "List of instances to strip banners from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :background_removal,
|
||||
description:
|
||||
"List of instances to strip user backgrounds from and the reason for doing so"
|
||||
},
|
||||
%{
|
||||
key: :reject_deletes,
|
||||
description: "List of instances to reject deletions from and the reason for doing so"
|
||||
|
|
|
@ -6,10 +6,54 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
|||
require Logger
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Emoji.Pack
|
||||
|
||||
@moduledoc "Detect new emojis by their shortcode and steals them"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
@pack_name "stolen"
|
||||
|
||||
# Config defaults
|
||||
@size_limit 50_000
|
||||
@download_unknown_size false
|
||||
|
||||
defp create_pack() do
|
||||
with {:ok, pack} = Pack.create(@pack_name) do
|
||||
Pack.save_metadata(
|
||||
%{
|
||||
"description" => "Collection of emoji auto-stolen from other instances",
|
||||
"homepage" => Pleroma.Web.Endpoint.url(),
|
||||
"can-download" => false,
|
||||
"share-files" => false
|
||||
},
|
||||
pack
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp load_or_create_pack() do
|
||||
case Pack.load_pack(@pack_name) do
|
||||
{:ok, pack} -> {:ok, pack}
|
||||
{:error, :enoent} -> create_pack()
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
defp add_emoji(shortcode, extension, filedata) do
|
||||
{:ok, pack} = load_or_create_pack()
|
||||
# Make final path infeasible to predict to thwart certain kinds of attacks
|
||||
# (48 bits is slighty more than 8 base62 chars, thus 9 chars)
|
||||
salt =
|
||||
:crypto.strong_rand_bytes(6)
|
||||
|> :crypto.bytes_to_integer()
|
||||
|> Base62.encode()
|
||||
|> String.pad_leading(9, "0")
|
||||
|
||||
filename = shortcode <> "-" <> salt <> "." <> extension
|
||||
|
||||
Pack.add_file(pack, shortcode, filename, filedata)
|
||||
end
|
||||
|
||||
defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
|
||||
|
||||
defp shortcode_matches?(shortcode, pattern) when is_binary(pattern) do
|
||||
|
@ -20,30 +64,69 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
|||
String.match?(shortcode, pattern)
|
||||
end
|
||||
|
||||
defp steal_emoji({shortcode, url}, emoji_dir_path) do
|
||||
defp reject_emoji?({shortcode, _url}, installed_emoji) do
|
||||
valid_shortcode? = String.match?(shortcode, ~r/^[a-zA-Z0-9_-]+$/)
|
||||
|
||||
rejected_shortcode? =
|
||||
[:mrf_steal_emoji, :rejected_shortcodes]
|
||||
|> Config.get([])
|
||||
|> Enum.any?(fn pattern -> shortcode_matches?(shortcode, pattern) end)
|
||||
|
||||
emoji_installed? = Enum.member?(installed_emoji, shortcode)
|
||||
|
||||
!valid_shortcode? or rejected_shortcode? or emoji_installed?
|
||||
end
|
||||
|
||||
defp steal_emoji(%{} = response, {shortcode, extension}) do
|
||||
case add_emoji(shortcode, extension, response.body) do
|
||||
{:ok, _} ->
|
||||
shortcode
|
||||
|
||||
e ->
|
||||
Logger.warning(
|
||||
"MRF.StealEmojiPolicy: Failed to add #{shortcode} as #{extension}: #{inspect(e)}"
|
||||
)
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp get_extension_if_safe(response) do
|
||||
content_type =
|
||||
:proplists.get_value("content-type", response.headers, MIME.from_path(response.url))
|
||||
|
||||
case content_type do
|
||||
"image/" <> _ -> List.first(MIME.extensions(content_type))
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp is_remote_size_within_limit?(url) do
|
||||
with {:ok, %{status: status, headers: headers} = _response} when status in 200..299 <-
|
||||
Pleroma.HTTP.request(:head, url, nil, [], []) do
|
||||
content_length = :proplists.get_value("content-length", headers, nil)
|
||||
size_limit = Config.get([:mrf_steal_emoji, :size_limit], @size_limit)
|
||||
|
||||
accept_unknown =
|
||||
Config.get([:mrf_steal_emoji, :download_unknown_size], @download_unknown_size)
|
||||
|
||||
content_length <= size_limit or
|
||||
(content_length == nil and accept_unknown)
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_steal_emoji({shortcode, url}) do
|
||||
url = Pleroma.Web.MediaProxy.url(url)
|
||||
|
||||
with {:ok, %{status: status} = response} when status in 200..299 <- Pleroma.HTTP.get(url) do
|
||||
size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
|
||||
with {:remote_size, true} <- {:remote_size, is_remote_size_within_limit?(url)},
|
||||
{:ok, %{status: status} = response} when status in 200..299 <- Pleroma.HTTP.get(url) do
|
||||
size_limit = Config.get([:mrf_steal_emoji, :size_limit], @size_limit)
|
||||
extension = get_extension_if_safe(response)
|
||||
|
||||
if byte_size(response.body) <= size_limit do
|
||||
extension =
|
||||
url
|
||||
|> URI.parse()
|
||||
|> Map.get(:path)
|
||||
|> Path.basename()
|
||||
|> Path.extname()
|
||||
|
||||
file_path = Path.join(emoji_dir_path, shortcode <> (extension || ".png"))
|
||||
|
||||
case File.write(file_path, response.body) do
|
||||
:ok ->
|
||||
shortcode
|
||||
|
||||
e ->
|
||||
Logger.warning("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
|
||||
nil
|
||||
end
|
||||
if byte_size(response.body) <= size_limit and extension do
|
||||
steal_emoji(response, {shortcode, extension})
|
||||
else
|
||||
Logger.debug(
|
||||
"MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{size_limit} B)"
|
||||
|
@ -65,26 +148,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
|||
if host != Pleroma.Web.Endpoint.host() and accept_host?(host) do
|
||||
installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
|
||||
|
||||
emoji_dir_path =
|
||||
Config.get(
|
||||
[:mrf_steal_emoji, :path],
|
||||
Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
|
||||
)
|
||||
|
||||
File.mkdir_p(emoji_dir_path)
|
||||
|
||||
new_emojis =
|
||||
foreign_emojis
|
||||
|> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end)
|
||||
|> Enum.filter(fn {shortcode, _url} ->
|
||||
reject_emoji? =
|
||||
[:mrf_steal_emoji, :rejected_shortcodes]
|
||||
|> Config.get([])
|
||||
|> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end)
|
||||
|
||||
!reject_emoji?
|
||||
end)
|
||||
|> Enum.map(&steal_emoji(&1, emoji_dir_path))
|
||||
|> Enum.reject(&reject_emoji?(&1, installed_emoji))
|
||||
|> Enum.map(&maybe_steal_emoji(&1))
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
if !Enum.empty?(new_emojis) do
|
||||
|
|
|
@ -53,6 +53,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
|
||||
defp fix_url(%{"url" => url} = data) when is_bitstring(url), do: data
|
||||
defp fix_url(%{"url" => url} = data) when is_map(url), do: Map.put(data, "url", url["href"])
|
||||
|
||||
defp fix_url(%{"url" => url} = data) when is_list(url) do
|
||||
data
|
||||
|> Map.put("url", List.first(url))
|
||||
|> fix_url()
|
||||
end
|
||||
|
||||
defp fix_url(data), do: data
|
||||
|
||||
defp fix_tag(%{"tag" => tag} = data) when is_list(tag) do
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
alias Pleroma.Emoji
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
@ -52,6 +53,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
defp fix(data) do
|
||||
data =
|
||||
data
|
||||
|> Transmogrifier.fix_tag()
|
||||
|> fix_emoji_qualification()
|
||||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_activity_addressing()
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
# Akkoma: Magically expressive social media
|
||||
# Copyright © 2024 Akkoma Authors <https://akkoma.dev/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do
|
||||
@moduledoc """
|
||||
Checks whether ActivityPub data represents a valid user
|
||||
|
||||
Users don't go through the same ingest pipeline like activities or other objects.
|
||||
To ensure this can only match a user and no users match in the other pipeline,
|
||||
this is a separate from the generic ObjectValidator.
|
||||
"""
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
|
||||
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.Signature
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
@impl true
|
||||
def validate(object, meta)
|
||||
|
||||
def validate(%{"type" => type, "id" => _id} = data, meta)
|
||||
when type in Pleroma.Constants.actor_types() do
|
||||
with :ok <- validate_pubkey(data),
|
||||
:ok <- validate_inbox(data),
|
||||
:ok <- contain_collection_origin(data) do
|
||||
{:ok, data, meta}
|
||||
else
|
||||
{:error, e} -> {:error, e}
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(_, _), do: {:error, "Not a user object"}
|
||||
|
||||
defp mabye_validate_owner(nil, _actor), do: :ok
|
||||
defp mabye_validate_owner(actor, actor), do: :ok
|
||||
defp mabye_validate_owner(_owner, _actor), do: :error
|
||||
|
||||
defp validate_pubkey(
|
||||
%{"id" => id, "publicKey" => %{"id" => pk_id, "publicKeyPem" => _key}} = data
|
||||
)
|
||||
when id != nil do
|
||||
with {_, {:ok, kactor}} <- {:key, Signature.key_id_to_actor_id(pk_id)},
|
||||
true <- id == kactor,
|
||||
:ok <- mabye_validate_owner(Map.get(data, "owner"), id) do
|
||||
:ok
|
||||
else
|
||||
{:key, _} ->
|
||||
{:error, "Unable to determine actor id from key id"}
|
||||
|
||||
false ->
|
||||
{:error, "Key id does not relate to user id"}
|
||||
|
||||
_ ->
|
||||
{:error, "Actor does not own its public key"}
|
||||
end
|
||||
end
|
||||
|
||||
# pubkey is optional atm
|
||||
defp validate_pubkey(_data), do: :ok
|
||||
|
||||
defp validate_inbox(%{"id" => id, "inbox" => inbox}) do
|
||||
case Containment.same_origin(id, inbox) do
|
||||
:ok -> :ok
|
||||
:error -> {:error, "Inbox on different doamin"}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_inbox(_), do: {:error, "No inbox"}
|
||||
|
||||
defp check_field_value(%{"id" => id} = _data, value) do
|
||||
Containment.same_origin(id, value)
|
||||
end
|
||||
|
||||
defp maybe_check_field(data, field) do
|
||||
with val when val != nil <- data[field],
|
||||
:ok <- check_field_value(data, val) do
|
||||
:ok
|
||||
else
|
||||
nil -> :ok
|
||||
_ -> {:error, "#{field} on different domain"}
|
||||
end
|
||||
end
|
||||
|
||||
defp contain_collection_origin(data) do
|
||||
Enum.reduce(["followers", "following", "featured"], :ok, fn
|
||||
field, :ok -> maybe_check_field(data, field)
|
||||
_, error -> error
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -115,13 +115,18 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
def should_federate?(url) do
|
||||
%{host: host} = URI.parse(url)
|
||||
|
||||
with allowed <- allowed_instances(),
|
||||
with {nil, false} <- {nil, is_nil(host)},
|
||||
allowed <- allowed_instances(),
|
||||
false <- Enum.empty?(allowed) do
|
||||
allowed
|
||||
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|
||||
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
|
||||
|> Pleroma.Web.ActivityPub.MRF.subdomain_match?(host)
|
||||
else
|
||||
# oi!
|
||||
{nil, true} ->
|
||||
false
|
||||
|
||||
_ ->
|
||||
quarantined_instances =
|
||||
blocked_instances()
|
||||
|
|
|
@ -109,7 +109,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
%User{} = followed <- User.get_cached_by_ap_id(followed_user),
|
||||
{_, {:ok, _, _}, _, _} <-
|
||||
{:following, User.follow(follower, followed, :follow_pending), follower, followed} do
|
||||
if followed.local && !followed.is_locked do
|
||||
if followed.local && User.can_direct_follow_local(follower, followed) do
|
||||
{:ok, accept_data, _} = Builder.accept(followed, object)
|
||||
{:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true)
|
||||
end
|
||||
|
|
|
@ -25,8 +25,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
import Ecto.Query
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Modifies an incoming AP object (mastodon format) to our internal format.
|
||||
|
@ -58,21 +58,48 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def fix_summary(object), do: Map.put(object, "summary", "")
|
||||
|
||||
def fix_addressing_list(map, field) do
|
||||
addrs = map[field]
|
||||
|
||||
defp fix_addressing_list(addrs) do
|
||||
cond do
|
||||
is_list(addrs) ->
|
||||
Map.put(map, field, Enum.filter(addrs, &is_binary/1))
|
||||
|
||||
is_binary(addrs) ->
|
||||
Map.put(map, field, [addrs])
|
||||
|
||||
true ->
|
||||
Map.put(map, field, [])
|
||||
is_list(addrs) -> Enum.filter(addrs, &is_binary/1)
|
||||
is_binary(addrs) -> [addrs]
|
||||
true -> []
|
||||
end
|
||||
end
|
||||
|
||||
# Due to JSON-LD simply "Public" and "as:Public" are equivalent to the full URI
|
||||
# but to simplify later checks we only want to deal with one reperesentation internally
|
||||
defp normalise_addressing_public_list(map, all_fields)
|
||||
|
||||
defp normalise_addressing_public_list(%{} = map, [field | fields]) do
|
||||
full_uri = Pleroma.Constants.as_public()
|
||||
|
||||
map =
|
||||
if map[field] != nil do
|
||||
new_fval =
|
||||
map[field]
|
||||
|> fix_addressing_list()
|
||||
|> Enum.map(fn
|
||||
"Public" -> full_uri
|
||||
"as:Public" -> full_uri
|
||||
x -> x
|
||||
end)
|
||||
|
||||
Map.put(map, field, new_fval)
|
||||
else
|
||||
map
|
||||
end
|
||||
|
||||
normalise_addressing_public_list(map, fields)
|
||||
end
|
||||
|
||||
defp normalise_addressing_public_list(map, _) do
|
||||
map
|
||||
end
|
||||
|
||||
defp normalise_addressing_public(map) do
|
||||
normalise_addressing_public_list(map, ["to", "cc", "bto", "bcc"])
|
||||
end
|
||||
|
||||
# if directMessage flag is set to true, leave the addressing alone
|
||||
def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection),
|
||||
do: object
|
||||
|
@ -96,6 +123,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Map.put("cc", final_cc)
|
||||
end
|
||||
|
||||
def fix_addressing_list_key(map, field) do
|
||||
Map.put(map, field, fix_addressing_list(map[field]))
|
||||
end
|
||||
|
||||
def fix_addressing(object) do
|
||||
{:ok, %User{follower_address: follower_collection}} =
|
||||
object
|
||||
|
@ -103,10 +134,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> User.get_or_fetch_by_ap_id()
|
||||
|
||||
object
|
||||
|> fix_addressing_list("to")
|
||||
|> fix_addressing_list("cc")
|
||||
|> fix_addressing_list("bto")
|
||||
|> fix_addressing_list("bcc")
|
||||
|> fix_addressing_list_key("to")
|
||||
|> fix_addressing_list_key("cc")
|
||||
|> fix_addressing_list_key("bto")
|
||||
|> fix_addressing_list_key("bcc")
|
||||
|> fix_explicit_addressing(follower_collection)
|
||||
|> CommonFixes.fix_implicit_addressing(follower_collection)
|
||||
end
|
||||
|
@ -135,8 +166,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Map.put("context", replied_object.data["context"] || object["conversation"])
|
||||
|> Map.drop(["conversation", "inReplyToAtomUri"])
|
||||
else
|
||||
e ->
|
||||
Logger.warning("Couldn't fetch reply@#{inspect(in_reply_to_id)}, error: #{inspect(e)}")
|
||||
_ ->
|
||||
object
|
||||
end
|
||||
else
|
||||
|
@ -384,11 +414,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end)
|
||||
end
|
||||
|
||||
def handle_incoming(data, options \\ [])
|
||||
def handle_incoming(data, options \\ []) do
|
||||
data = normalise_addressing_public(data)
|
||||
|
||||
data =
|
||||
if data["object"] != nil do
|
||||
object = normalise_addressing_public(data["object"])
|
||||
Map.put(data, "object", object)
|
||||
else
|
||||
data
|
||||
end
|
||||
|
||||
handle_incoming_normalised(data, options)
|
||||
end
|
||||
|
||||
defp handle_incoming_normalised(data, options)
|
||||
|
||||
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
|
||||
# with nil ID.
|
||||
def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
|
||||
defp handle_incoming_normalised(
|
||||
%{"type" => "Flag", "object" => objects, "actor" => actor} = data,
|
||||
_options
|
||||
) do
|
||||
with context <- data["context"] || Utils.generate_context_id(),
|
||||
content <- data["content"] || "",
|
||||
%User{} = actor <- User.get_cached_by_ap_id(actor),
|
||||
|
@ -409,20 +456,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
# disallow objects with bogus IDs
|
||||
def handle_incoming(%{"id" => nil}, _options), do: :error
|
||||
def handle_incoming(%{"id" => ""}, _options), do: :error
|
||||
defp handle_incoming_normalised(%{"id" => nil}, _options), do: :error
|
||||
defp handle_incoming_normalised(%{"id" => ""}, _options), do: :error
|
||||
# length of https:// = 8, should validate better, but good enough for now.
|
||||
def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
|
||||
do: :error
|
||||
defp handle_incoming_normalised(%{"id" => id}, _options)
|
||||
when is_binary(id) and byte_size(id) < 8,
|
||||
do: :error
|
||||
|
||||
@doc "Rewrite misskey likes into EmojiReacts"
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Like",
|
||||
"content" => reaction
|
||||
} = data,
|
||||
options
|
||||
) do
|
||||
# Rewrite misskey likes into EmojiReacts
|
||||
defp handle_incoming_normalised(
|
||||
%{
|
||||
"type" => "Like",
|
||||
"content" => reaction
|
||||
} = data,
|
||||
options
|
||||
) do
|
||||
if Pleroma.Emoji.is_unicode_emoji?(reaction) || Pleroma.Emoji.matches_shortcode?(reaction) do
|
||||
data
|
||||
|> Map.put("type", "EmojiReact")
|
||||
|
@ -434,11 +482,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
|
||||
options
|
||||
)
|
||||
when objtype in ~w{Question Answer Audio Video Event Article Note Page} do
|
||||
defp handle_incoming_normalised(
|
||||
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
|
||||
options
|
||||
)
|
||||
when objtype in ~w{Question Answer Audio Video Event Article Note Page} do
|
||||
fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
|
||||
|
||||
object =
|
||||
|
@ -470,8 +518,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(%{"type" => type} = data, _options)
|
||||
when type in ~w{Like EmojiReact Announce Add Remove} do
|
||||
defp handle_incoming_normalised(%{"type" => type} = data, _options)
|
||||
when type in ~w{Like EmojiReact Announce Add Remove} do
|
||||
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
||||
{:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
|
@ -481,11 +529,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => type} = data,
|
||||
_options
|
||||
)
|
||||
when type in ~w{Update Block Follow Accept Reject} do
|
||||
defp handle_incoming_normalised(
|
||||
%{"type" => type} = data,
|
||||
_options
|
||||
)
|
||||
when type in ~w{Update Block Follow Accept Reject} do
|
||||
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
|
||||
{:ok, activity, _} <-
|
||||
Pipeline.common_pipeline(data, local: false) do
|
||||
|
@ -493,10 +541,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Delete"} = data,
|
||||
_options
|
||||
) do
|
||||
defp handle_incoming_normalised(
|
||||
%{"type" => "Delete"} = data,
|
||||
_options
|
||||
) do
|
||||
with {:ok, activity, _} <-
|
||||
Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
|
@ -516,15 +564,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => "Follow", "object" => followed},
|
||||
"actor" => follower,
|
||||
"id" => id
|
||||
} = _data,
|
||||
_options
|
||||
) do
|
||||
defp handle_incoming_normalised(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => "Follow", "object" => followed},
|
||||
"actor" => follower,
|
||||
"id" => id
|
||||
} = _data,
|
||||
_options
|
||||
) do
|
||||
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
|
||||
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
|
||||
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
|
||||
|
@ -535,28 +583,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => type}
|
||||
} = data,
|
||||
_options
|
||||
)
|
||||
when type in ["Like", "EmojiReact", "Announce", "Block"] do
|
||||
defp handle_incoming_normalised(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => type}
|
||||
} = data,
|
||||
_options
|
||||
)
|
||||
when type in ["Like", "EmojiReact", "Announce", "Block"] do
|
||||
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
# For Undos that don't have the complete object attached, try to find it in our database.
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => object
|
||||
} = activity,
|
||||
options
|
||||
)
|
||||
when is_binary(object) do
|
||||
defp handle_incoming_normalised(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
"object" => object
|
||||
} = activity,
|
||||
options
|
||||
)
|
||||
when is_binary(object) do
|
||||
with %Activity{data: data} <- Activity.get_by_ap_id(object) do
|
||||
activity
|
||||
|> Map.put("object", data)
|
||||
|
@ -566,17 +614,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Move",
|
||||
"actor" => origin_actor,
|
||||
"object" => origin_actor,
|
||||
"target" => target_actor
|
||||
},
|
||||
_options
|
||||
) do
|
||||
defp handle_incoming_normalised(
|
||||
%{
|
||||
"type" => "Move",
|
||||
"actor" => origin_actor,
|
||||
"object" => origin_actor,
|
||||
"target" => target_actor
|
||||
},
|
||||
_options
|
||||
) do
|
||||
with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
|
||||
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
|
||||
# Use a dramatically shortened maximum age before refresh here because it is reasonable
|
||||
# for a user to
|
||||
# 1. Add the alias to their new account and then
|
||||
# 2. Press the button on their new account
|
||||
# within a very short period of time and expect it to work
|
||||
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor, maximum_age: 5),
|
||||
true <- origin_actor in target_user.also_known_as do
|
||||
ActivityPub.move(origin_user, target_user, false)
|
||||
else
|
||||
|
@ -584,7 +637,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_incoming(_, _), do: :error
|
||||
defp handle_incoming_normalised(_, _), do: :error
|
||||
|
||||
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
|
||||
def get_obj_helper(id, options \\ []) do
|
||||
|
@ -828,8 +881,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
relative_object do
|
||||
Map.put(data, "object", external_url)
|
||||
else
|
||||
{:fetch, e} ->
|
||||
Logger.error("Couldn't fetch fixed_object@#{object} #{inspect(e)}")
|
||||
{:fetch, _} ->
|
||||
data
|
||||
|
||||
_ ->
|
||||
|
|
|
@ -16,10 +16,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
use Pleroma.Web, :verified_routes
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
|
@ -124,19 +125,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
end
|
||||
|
||||
def generate_activity_id do
|
||||
generate_id("activities")
|
||||
url(~p[/activities/#{UUID.generate()}])
|
||||
end
|
||||
|
||||
def generate_context_id do
|
||||
generate_id("contexts")
|
||||
url(~p[/contexts/#{UUID.generate()}])
|
||||
end
|
||||
|
||||
def generate_object_id do
|
||||
Helpers.o_status_url(Endpoint, :object, UUID.generate())
|
||||
end
|
||||
|
||||
def generate_id(type) do
|
||||
"#{Endpoint.url()}/#{type}/#{UUID.generate()}"
|
||||
url(~p[/objects/#{UUID.generate()}])
|
||||
end
|
||||
|
||||
def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
|
||||
|
@ -154,7 +151,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
Notification.get_notified_from_activity(%Activity{data: object}, false)
|
||||
end
|
||||
|
||||
def maybe_create_context(context), do: context || generate_id("contexts")
|
||||
def maybe_create_context(context), do: context || generate_context_id()
|
||||
|
||||
@doc """
|
||||
Enqueues an activity for federation if it's local
|
||||
|
|
|
@ -12,24 +12,21 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
require Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do
|
||||
%{"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)}
|
||||
%{"sharedInbox" => url(~p"/inbox")}
|
||||
end
|
||||
|
||||
def render("endpoints.json", %{user: %User{local: true} = _user}) do
|
||||
%{
|
||||
"oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
|
||||
"oauthRegistrationEndpoint" => Helpers.app_url(Endpoint, :create),
|
||||
"oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
|
||||
"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox),
|
||||
"uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media)
|
||||
"oauthAuthorizationEndpoint" => url(~p"/oauth/authorize"),
|
||||
"oauthRegistrationEndpoint" => url(~p"/api/v1/apps"),
|
||||
"oauthTokenEndpoint" => url(~p"/oauth/token"),
|
||||
"sharedInbox" => url(~p"/inbox")
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -48,6 +45,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"following" => "#{user.ap_id}/following",
|
||||
"followers" => "#{user.ap_id}/followers",
|
||||
"inbox" => "#{user.ap_id}/inbox",
|
||||
"outbox" => "#{user.ap_id}/outbox",
|
||||
"name" => "Pleroma",
|
||||
"summary" =>
|
||||
"An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
|
||||
|
@ -113,6 +111,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
}
|
||||
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|
||||
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
|
||||
# Yes, the key is named ...Url eventhough it is a whole 'Image' object
|
||||
|> Map.merge(maybe_insert_image("backgroundUrl", User.background_url(user)))
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
@ -288,7 +288,12 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
end
|
||||
|
||||
defp maybe_make_image(func, key, user) do
|
||||
if image = func.(user, no_default: true) do
|
||||
image = func.(user, no_default: true)
|
||||
maybe_insert_image(key, image)
|
||||
end
|
||||
|
||||
defp maybe_insert_image(key, image) do
|
||||
if image do
|
||||
%{
|
||||
key => %{
|
||||
"type" => "Image",
|
||||
|
|
|
@ -17,9 +17,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
alias Pleroma.Web.AdminAPI
|
||||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
alias Pleroma.Web.AdminAPI.ModerationLogView
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Web.Router
|
||||
|
||||
@users_page_size 50
|
||||
|
||||
|
@ -256,7 +254,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
conn
|
||||
|> json(%{
|
||||
token: token.token,
|
||||
link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
|
||||
link: url(~p[/api/v1/pleroma/password_reset/#{token.token}])
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
@ -451,6 +451,20 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
}
|
||||
end
|
||||
|
||||
def preferences_operation do
|
||||
%Operation{
|
||||
tags: ["Account Preferences"],
|
||||
description: "Preferences defined by the user in their account settings.",
|
||||
summary: "Preferred common behaviors to be shared across clients.",
|
||||
operationId: "AccountController.preferences",
|
||||
security: [%{"oAuth" => ["read:accounts"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Preferences", "application/json", Account),
|
||||
401 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def identity_proofs_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve account information"],
|
||||
|
@ -709,6 +723,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
description:
|
||||
"Number of days after which statuses will be deleted. Set to -1 to disable."
|
||||
},
|
||||
permit_followback: %Schema{
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description:
|
||||
"Whether follow requests from accounts the user is already following are auto-approved (when locked)."
|
||||
},
|
||||
accepts_direct_messages_from: %Schema{
|
||||
type: :string,
|
||||
enum: [
|
||||
|
@ -740,6 +760,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
discoverable: false,
|
||||
actor_type: "Person",
|
||||
status_ttl_days: 30,
|
||||
permit_followback: true,
|
||||
accepts_direct_messages_from: "everybody"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,9 +111,9 @@ defmodule Pleroma.Web.ApiSpec.FrontendSettingsOperation do
|
|||
def update_preferred_frontend_operation() do
|
||||
%Operation{
|
||||
tags: ["Frontends"],
|
||||
summary: "Frontend Settings Profiles",
|
||||
description: "List frontend setting profiles",
|
||||
operationId: "AkkomaAPI.FrontendSettingsController.available_frontends",
|
||||
summary: "Update preferred frontend setting",
|
||||
description: "Store preferred frontend in cookies",
|
||||
operationId: "AkkomaAPI.FrontendSettingsController.update_preferred_frontend",
|
||||
requestBody:
|
||||
request_body(
|
||||
"Frontend",
|
||||
|
@ -132,9 +132,11 @@ defmodule Pleroma.Web.ApiSpec.FrontendSettingsOperation do
|
|||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Frontends", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: %Schema{
|
||||
type: :string
|
||||
type: :object,
|
||||
properties: %{
|
||||
frontend_name: %Schema{
|
||||
type: :string
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -137,7 +137,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
"background_upload_limit" => 4_000_000,
|
||||
"background_image" => "/static/image.png",
|
||||
"banner_upload_limit" => 4_000_000,
|
||||
"description" => "Pleroma: An efficient and flexible fediverse server",
|
||||
"description" => "Akkoma: The cooler fediverse server",
|
||||
"email" => "lain@lain.com",
|
||||
"languages" => ["en"],
|
||||
"max_toot_chars" => 5000,
|
||||
|
@ -160,7 +160,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
"urls" => %{
|
||||
"streaming_api" => "wss://lain.com"
|
||||
},
|
||||
"version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)"
|
||||
"version" => "2.7.2 (compatible; Akkoma 3.9.3-232-g6fde75e1-develop)"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
@ -150,7 +150,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
"removes the contents of a message from the push notification"
|
||||
)
|
||||
],
|
||||
requestBody: nil,
|
||||
requestBody: request_body("Parameters", update_notification_settings_request()),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Success", "application/json", %Schema{
|
||||
|
@ -432,4 +432,22 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_notification_settings_request do
|
||||
%Schema{
|
||||
title: "UpdateNotificationSettings",
|
||||
description: "PUT paramenters (query, form or JSON) for updating notification settings",
|
||||
type: :object,
|
||||
properties: %{
|
||||
block_from_strangers: %Schema{
|
||||
type: :boolean,
|
||||
description: "blocks notifications from accounts you do not follow"
|
||||
},
|
||||
hide_notification_contents: %Schema{
|
||||
type: :boolean,
|
||||
description: "removes the contents of a message from the push notification"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -112,7 +112,18 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
|||
akkoma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
note_ttl_days: %Schema{type: :integer}
|
||||
instance: %Schema{
|
||||
type: :object,
|
||||
nullable: true,
|
||||
properties: %{
|
||||
name: %Schema{type: :string},
|
||||
favicon: %Schema{type: :string, format: :uri, nullable: true},
|
||||
# XXX: proper nodeinfo schema
|
||||
nodeinfo: %Schema{type: :object, nullable: true}
|
||||
}
|
||||
},
|
||||
status_ttl_days: %Schema{type: :integer, nullable: true},
|
||||
permit_followback: %Schema{type: :boolean}
|
||||
}
|
||||
},
|
||||
source: %Schema{
|
||||
|
@ -205,6 +216,18 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
|
|||
"pleroma-fe" => %{}
|
||||
}
|
||||
},
|
||||
"akkoma" => %{
|
||||
"instance" => %{
|
||||
"name" => "ihatebeinga.live",
|
||||
"favicon" => "https://ihatebeinga.live/favicon.png",
|
||||
"nodeinfo" =>
|
||||
%{
|
||||
# XXX: nodeinfo schema
|
||||
}
|
||||
},
|
||||
"status_ttl_days" => nil,
|
||||
"permit_followback" => true
|
||||
},
|
||||
"source" => %{
|
||||
"fields" => [],
|
||||
"note" => "foobar",
|
||||
|
|
|
@ -97,7 +97,11 @@ defmodule Pleroma.Web.Endpoint do
|
|||
Plug.Static,
|
||||
at: "/",
|
||||
from: :pleroma,
|
||||
only: Pleroma.Constants.static_only_files(),
|
||||
only: Pleroma.Web.static_paths(),
|
||||
# JSON-LD is accepted by some servers for AP objects and activities,
|
||||
# thus only enable it here instead of a global extension mapping
|
||||
# (it's our only *.jsonld file anyway)
|
||||
content_types: %{"litepub-0.1.jsonld" => "application/ld+json"},
|
||||
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
||||
gzip: true,
|
||||
cache_control_for_etags: @static_cache_control,
|
||||
|
|
|
@ -78,9 +78,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
end
|
||||
|
||||
def activity_content(%{"content" => content}) do
|
||||
content
|
||||
|> String.replace(~r/[\n\r]/, "")
|
||||
|> escape()
|
||||
escape(content)
|
||||
end
|
||||
|
||||
def activity_content(_), do: ""
|
||||
|
|
|
@ -30,7 +30,7 @@ defmodule Pleroma.Web.Feed.UserController do
|
|||
|
||||
def feed_redirect(conn, %{"nickname" => nickname}) do
|
||||
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
|
||||
redirect(conn, external: "#{Routes.user_feed_url(conn, :feed, user.nickname)}.atom")
|
||||
redirect(conn, external: "#{url(~p"/users/#{user.nickname}/feed")}.atom")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -34,9 +34,9 @@ defmodule Pleroma.Web.MastoFEController do
|
|||
|
||||
index =
|
||||
if flavour == "fedibird-fe" do
|
||||
"fedibird.index.html"
|
||||
"fedibird.html"
|
||||
else
|
||||
"glitchsoc.index.html"
|
||||
"glitchsoc.html"
|
||||
end
|
||||
|
||||
conn
|
||||
|
|
|
@ -51,7 +51,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:accounts"]}
|
||||
when action in [:verify_credentials, :endorsements, :identity_proofs]
|
||||
when action in [:verify_credentials, :endorsements, :identity_proofs, :preferences]
|
||||
)
|
||||
|
||||
plug(
|
||||
|
@ -222,6 +222,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
|
||||
|> Maps.put_if_present(:status_ttl_days, params[:status_ttl_days], status_ttl_days_value)
|
||||
|> Maps.put_if_present(:accepts_direct_messages_from, params[:accepts_direct_messages_from])
|
||||
|> Maps.put_if_present(:permit_followback, params[:permit_followback])
|
||||
|
||||
# What happens here:
|
||||
#
|
||||
|
@ -544,4 +545,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
@doc "GET /api/v1/identity_proofs"
|
||||
def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
|
||||
|
||||
@doc "GET /api/v1/preferences"
|
||||
def preferences(%{assigns: %{user: user}} = conn, _params) do
|
||||
render(conn, "preferences.json", user: user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,12 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
|
|||
defp redirect_to_oauth_form(conn, _params) do
|
||||
with {:ok, app} <- local_mastofe_app() do
|
||||
path =
|
||||
Routes.o_auth_path(conn, :authorize,
|
||||
response_type: "code",
|
||||
client_id: app.client_id,
|
||||
redirect_uri: ".",
|
||||
scope: Enum.join(app.scopes, " ")
|
||||
)
|
||||
~p[/oauth/authorize?#{[response_type: "code", client_id: app.client_id, redirect_uri: ".", scope: Enum.join(app.scopes, " ")]}]
|
||||
|
||||
redirect(conn, to: path)
|
||||
end
|
||||
|
@ -91,7 +86,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
|
|||
defp local_mastodon_post_login_path(conn) do
|
||||
case get_session(conn, :return_to) do
|
||||
nil ->
|
||||
Routes.masto_fe_path(conn, :index, ["getting-started"])
|
||||
~p"/web/getting-started"
|
||||
|
||||
return_to ->
|
||||
delete_session(conn, :return_to)
|
||||
|
|
|
@ -190,6 +190,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
|
||||
def render("instance.json", _), do: nil
|
||||
|
||||
def render("preferences.json", %{user: user} = _opts) do
|
||||
# TODO: Do we expose more settings that make sense to plug in here?
|
||||
%{
|
||||
"posting:default:visibility": user.default_scope,
|
||||
"posting:default:sensitive": false,
|
||||
"posting:default:language": nil,
|
||||
"reading:expand:media": "default",
|
||||
"reading:expand:spoilers": false
|
||||
}
|
||||
end
|
||||
|
||||
defp do_render("show.json", %{user: user} = opts) do
|
||||
user = User.sanitize_html(user, User.html_filter_policy(opts[:for]))
|
||||
display_name = user.name || user.nickname
|
||||
|
@ -250,6 +261,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
|> MediaProxy.url()
|
||||
end
|
||||
|
||||
last_status_at =
|
||||
if is_nil(user.last_status_at), do: nil, else: NaiveDateTime.to_date(user.last_status_at)
|
||||
|
||||
%{
|
||||
id: to_string(user.id),
|
||||
username: username_from_nickname(user.nickname),
|
||||
|
@ -278,10 +292,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
actor_type: user.actor_type
|
||||
}
|
||||
},
|
||||
last_status_at: user.last_status_at,
|
||||
last_status_at: last_status_at,
|
||||
akkoma: %{
|
||||
instance: render("instance.json", %{instance: instance}),
|
||||
status_ttl_days: user.status_ttl_days
|
||||
status_ttl_days: user.status_ttl_days,
|
||||
permit_followback: user.permit_followback
|
||||
},
|
||||
# Pleroma extensions
|
||||
# Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub
|
||||
|
|
|
@ -322,7 +322,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
|
||||
url =
|
||||
if user.local do
|
||||
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
|
||||
url(~p[/notice/#{activity}])
|
||||
else
|
||||
object.data["url"] || object.data["external_url"] || object.data["id"]
|
||||
end
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.TagView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
def render("index.json", %{tags: tags, for_user: user}) do
|
||||
render_many(tags, __MODULE__, "show.json", %{for_user: user})
|
||||
|
@ -17,7 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.TagView do
|
|||
|
||||
%{
|
||||
name: tag.name,
|
||||
url: Helpers.tag_feed_url(Pleroma.Web.Endpoint, :feed, tag.name),
|
||||
url: url(~p[/tags/#{tag.name}]),
|
||||
history: [],
|
||||
following: following
|
||||
}
|
||||
|
|
|
@ -145,7 +145,7 @@ defmodule Pleroma.Web.MediaProxy do
|
|||
end
|
||||
|
||||
def base_url do
|
||||
Config.get([:media_proxy, :base_url], Endpoint.url())
|
||||
Config.get!([:media_proxy, :base_url])
|
||||
end
|
||||
|
||||
defp proxy_url(path, sig_base64, url_base64, filename) do
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue