forked from AkkomaGang/akkoma
Compare commits
261 commits
Author | SHA1 | Date | |
---|---|---|---|
floatingghost | 8afc3bee7a | ||
floatingghost | 72871d4514 | ||
floatingghost | 72af38c0e9 | ||
Floatingghost | ae19fd90c9 | ||
Floatingghost | 66b3248dd3 | ||
Floatingghost | 73ead8656a | ||
Floatingghost | f32a7fd76a | ||
Floatingghost | 4078fd655c | ||
floatingghost | 5bdef8c724 | ||
floatingghost | cdc918c8f1 | ||
Floatingghost | f15eded3e1 | ||
Oneric | 05eda169fe | ||
floatingghost | 3ce855cbde | ||
Floatingghost | da67e69af5 | ||
Norm | c2d3221be3 | ||
Floatingghost | 5e92f955ac | ||
Floatingghost | b72127b45a | ||
Oneric | 9a91299f96 | ||
Oneric | fbd961c747 | ||
Oneric | 0c2b33458d | ||
Floatingghost | 842cac2a50 | ||
3e1f5e5372 | |||
3a21293970 | |||
0d66237205 | |||
Oneric | 6ef6b2a289 | ||
Oneric | 94e9c8f48a | ||
Oneric | 873aa9da1c | ||
Oneric | 34a48cb87f | ||
50403351f4 | |||
a953b1d927 | |||
Norm | bb29c5bed2 | ||
Norm | bc46f3da4c | ||
Norm | 7e709768c3 | ||
floatingghost | 76ded10a70 | ||
Floatingghost | 4457928e32 | ||
floatingghost | ee03149ba1 | ||
Floatingghost | ea6bc8a7c5 | ||
Floatingghost | bd74693db6 | ||
Oneric | 5256678901 | ||
floatingghost | fdeecc7b4c | ||
floatingghost | 51482c4fe8 | ||
Oneric | b7e3d44756 | ||
Norm | 8ae54b260a | ||
Floatingghost | 21a81e1111 | ||
Floatingghost | 3738ab67bd | ||
Floatingghost | 7038b60ab5 | ||
Norm | 549d580054 | ||
Floatingghost | 010e8c7bb2 | ||
Floatingghost | 9671cdecdf | ||
Floatingghost | f531484063 | ||
Floatingghost | ec7e9da734 | ||
FloatingGhost | 3c384c1b76 | ||
FloatingGhost | 2437a3e9ba | ||
FloatingGhost | ad7dcf38a8 | ||
91d9d750c0 | |||
3c07aa506d | |||
64050b0fb5 | |||
7babc11475 | |||
Floatingghost | 828158ef49 | ||
Floatingghost | c7276713e0 | ||
floatingghost | 310c1b7e24 | ||
floatingghost | 7da6f41718 | ||
floatingghost | 53c67993bb | ||
Oneric | 5bc64c5753 | ||
Oneric | 5ee0fb18cb | ||
Norm | 771a306dc1 | ||
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 | ||
0c2f200b4d | |||
3f54945033 | |||
09d3ccf770 | |||
9da0fe930e | |||
2a9db73b4c | |||
floatingghost | 0fee71f58f | ||
Floatingghost | 370576474c | ||
Floatingghost | 1ed975636b | ||
cd7af81896 | |||
Floatingghost | 2c7e5b2287 | ||
Floatingghost | ddb8a5ef73 | ||
Floatingghost | 123db1abc4 | ||
Floatingghost | b2c29527fb | ||
59d32c10d9 | |||
Floatingghost | d2cee15c15 | ||
Floatingghost | d70fa16383 | ||
Floatingghost | 5043571084 | ||
Floatingghost | 1896ff1ab0 | ||
Floatingghost | b7dd739de1 | ||
b144218dce | |||
Floatingghost | 2fc25980d1 | ||
floatingghost | c1f0b6b875 | ||
Floatingghost | 18442dcc7e | ||
Floatingghost | 33fb74043d | ||
Floatingghost | 49ed27cd96 | ||
Floatingghost | 7f6e35ece4 | ||
2e369aef71 | |||
fed7a78c77 | |||
c0532bcae0 | |||
f31b262aec | |||
ff515c05c3 | |||
7e5004b3e2 | |||
53a9413b95 | |||
d69cba1b93 | |||
3c54f407c5 | |||
825ae46bfa | |||
331710b6bb | |||
eeed051a0f | |||
30d63aaa6e | |||
e2b04fac5a | |||
6d368808d3 | |||
160d113b30 | |||
132036f951 | |||
4ff22a409a | |||
4c29366fe5 | |||
ac4cc619ea | |||
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 | 9c53a3390e | ||
FloatingGhost | 795524daf1 | ||
FloatingGhost | b5d97e7d85 | ||
FloatingGhost | f592090206 | ||
FloatingGhost | 61621ebdbc | ||
FloatingGhost | 4cd299bd83 | ||
Erin Shepherd | 8fbd771d6e | ||
Erin Shepherd | 464db9ea0b | ||
FloatingGhost | 2d439034ca | ||
FloatingGhost | 087d88f787 | ||
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 | ||
5d89e0c917 | |||
Erin Shepherd | f18e2ba42c | ||
Oneric | fc95519dbf | ||
FloatingGhost | 889b57df82 | ||
34ffb92db4 | |||
c6dceb1802 | |||
caaf2deb22 | |||
floatingghost | 7d61fb0906 | ||
floatingghost | cdf73e0ac8 | ||
floatingghost | 967e6b8ade | ||
Oneric | d7c8e9df27 | ||
Oneric | a0daec6ea1 | ||
Oneric | bff2812a93 | ||
Oneric | 7964272c98 | ||
Oneric | c08f49d88e | ||
FloatingGhost | 3111181d3c | ||
floatingghost | 2f9aad0e65 | ||
Erin Shepherd | b387f4a1c1 | ||
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 | ||
Oneric | 192480093c | ||
Norm | 0cb3812ac0 | ||
66a04cead3 | |||
f50cffd134 | |||
338612d72b |
|
@ -1 +0,0 @@
|
||||||
https://github.com/hashnuke/heroku-buildpack-elixir
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -78,3 +78,4 @@ docs/venv
|
||||||
# docker stuff
|
# docker stuff
|
||||||
docker-db
|
docker-db
|
||||||
*.iml
|
*.iml
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
platform: linux/amd64
|
labels:
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- test
|
- test
|
||||||
|
@ -34,7 +35,7 @@ variables:
|
||||||
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
||||||
- &mix-clean "mix deps.clean --all && mix clean"
|
- &mix-clean "mix deps.clean --all && mix clean"
|
||||||
|
|
||||||
pipeline:
|
steps:
|
||||||
# Canonical amd64
|
# Canonical amd64
|
||||||
debian-bookworm:
|
debian-bookworm:
|
||||||
image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612
|
image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
platform: linux/arm64
|
labels:
|
||||||
|
platform: linux/aarch64
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- test
|
- test
|
||||||
|
@ -34,7 +35,7 @@ variables:
|
||||||
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
||||||
- &mix-clean "mix deps.clean --all && mix clean"
|
- &mix-clean "mix deps.clean --all && mix clean"
|
||||||
|
|
||||||
pipeline:
|
steps:
|
||||||
# Canonical arm64
|
# Canonical arm64
|
||||||
debian-bookworm:
|
debian-bookworm:
|
||||||
image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612
|
image: hexpm/elixir:1.15.4-erlang-26.0.2-debian-bookworm-20230612
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
platform: linux/amd64
|
labels:
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- test
|
- test
|
||||||
|
@ -45,7 +46,7 @@ variables:
|
||||||
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
||||||
- &mix-clean "mix deps.clean --all && mix clean"
|
- &mix-clean "mix deps.clean --all && mix clean"
|
||||||
|
|
||||||
pipeline:
|
steps:
|
||||||
docs:
|
docs:
|
||||||
<<: *on-point-release
|
<<: *on-point-release
|
||||||
secrets:
|
secrets:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
platform: linux/amd64
|
labels:
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- &scw-secrets
|
- &scw-secrets
|
||||||
|
@ -41,9 +42,9 @@ variables:
|
||||||
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
|
||||||
- &mix-clean "mix deps.clean --all && mix clean"
|
- &mix-clean "mix deps.clean --all && mix clean"
|
||||||
|
|
||||||
pipeline:
|
steps:
|
||||||
lint:
|
lint:
|
||||||
image: akkoma/ci-base:1.15-otp26
|
image: akkoma/ci-base:1.16-otp26
|
||||||
<<: *on-pr-open
|
<<: *on-pr-open
|
||||||
environment:
|
environment:
|
||||||
MIX_ENV: test
|
MIX_ENV: test
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
platform: linux/amd64
|
labels:
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- lint
|
- lint
|
||||||
|
@ -7,15 +8,12 @@ matrix:
|
||||||
ELIXIR_VERSION:
|
ELIXIR_VERSION:
|
||||||
- 1.14
|
- 1.14
|
||||||
- 1.15
|
- 1.15
|
||||||
|
- 1.16
|
||||||
OTP_VERSION:
|
OTP_VERSION:
|
||||||
- 25
|
- 25
|
||||||
- 26
|
- 26
|
||||||
include:
|
include:
|
||||||
- ELIXIR_VERSION: 1.14
|
- ELIXIR_VERSION: 1.16
|
||||||
OTP_VERSION: 25
|
|
||||||
- ELIXIR_VERSION: 1.15
|
|
||||||
OTP_VERSION: 25
|
|
||||||
- ELIXIR_VERSION: 1.15
|
|
||||||
OTP_VERSION: 26
|
OTP_VERSION: 26
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
|
@ -70,7 +68,7 @@ services:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
|
|
||||||
pipeline:
|
steps:
|
||||||
test:
|
test:
|
||||||
image: akkoma/ci-base:${ELIXIR_VERSION}-otp${OTP_VERSION}
|
image: akkoma/ci-base:${ELIXIR_VERSION}-otp${OTP_VERSION}
|
||||||
<<: *on-pr-open
|
<<: *on-pr-open
|
||||||
|
|
90
CHANGELOG.md
90
CHANGELOG.md
|
@ -4,7 +4,95 @@ 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/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Unreleased
|
## 2024.04.1 (Security)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- Issue allowing non-owners to use media objects in posts
|
||||||
|
- Issue allowing use of non-media objects as attachments and crashing timeline rendering
|
||||||
|
- Issue allowing webfinger spoofing in certain situations
|
||||||
|
|
||||||
|
## Added
|
||||||
|
- Implement [FEP-67ff](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) (federation documentation)
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- Ratelimits encountered when fetching objects are now respected; 429 responses will cause a backoff when we get one.
|
||||||
|
|
||||||
|
## 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
|
## Added
|
||||||
- Full compatibility with Erlang OTP26
|
- Full compatibility with Erlang OTP26
|
||||||
|
|
42
FEDERATION.md
Normal file
42
FEDERATION.md
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# Federation
|
||||||
|
|
||||||
|
## Supported federation protocols and standards
|
||||||
|
|
||||||
|
- [ActivityPub](https://www.w3.org/TR/activitypub/) (Server-to-Server)
|
||||||
|
- [WebFinger](https://webfinger.net/)
|
||||||
|
- [Http Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
|
||||||
|
- [NodeInfo](https://nodeinfo.diaspora.software/)
|
||||||
|
|
||||||
|
## Supported FEPs
|
||||||
|
|
||||||
|
- [FEP-67ff: FEDERATION](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md)
|
||||||
|
- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)
|
||||||
|
- [FEP-fffd: Proxy Objects](https://codeberg.org/fediverse/fep/src/branch/main/fep/fffd/fep-fffd.md)
|
||||||
|
|
||||||
|
## ActivityPub
|
||||||
|
|
||||||
|
Akkoma mostly follows the server-to-server parts of the ActivityPub standard,
|
||||||
|
but implements quirks for Mastodon compatibility as well as Mastodon-specific
|
||||||
|
and custom extensions.
|
||||||
|
|
||||||
|
See our documentation and Mastodon’s federation information
|
||||||
|
linked further below for details on these quirks and extensions.
|
||||||
|
|
||||||
|
Akkoma does not perform JSON-LD processing.
|
||||||
|
|
||||||
|
### Required extensions
|
||||||
|
|
||||||
|
#### HTTP Signatures
|
||||||
|
All AP S2S POST requests to Akkoma instances MUST be signed.
|
||||||
|
Depending on instance configuration the same may be true for GET requests.
|
||||||
|
|
||||||
|
## Nodeinfo
|
||||||
|
|
||||||
|
Akkoma provides many additional entries in its nodeinfo response,
|
||||||
|
see the documentation linked below for details.
|
||||||
|
|
||||||
|
## Additional documentation
|
||||||
|
|
||||||
|
- [Akkoma’s ActivityPub extensions](https://docs.akkoma.dev/develop/development/ap_extensions/)
|
||||||
|
- [Akkoma’s nodeinfo extensions](https://docs.akkoma.dev/develop/development/nodeinfo_extensions/)
|
||||||
|
- [Mastodon’s federation requirements](https://github.com/mastodon/mastodon/blob/main/FEDERATION.md)
|
27
SECURITY.md
27
SECURITY.md
|
@ -1,16 +1,21 @@
|
||||||
# Pleroma backend security policy
|
# Akkoma backend security handling
|
||||||
|
|
||||||
## Supported versions
|
|
||||||
|
|
||||||
Currently, Pleroma offers bugfixes and security patches only for the latest minor release.
|
|
||||||
|
|
||||||
| Version | Support
|
|
||||||
|---------| --------
|
|
||||||
| 2.2 | Bugfixes and security patches
|
|
||||||
|
|
||||||
## Reporting a vulnerability
|
## 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
|
## 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
|
# Upload configuration
|
||||||
config :pleroma, Pleroma.Upload,
|
config :pleroma, Pleroma.Upload,
|
||||||
uploader: Pleroma.Uploaders.Local,
|
uploader: Pleroma.Uploaders.Local,
|
||||||
filters: [Pleroma.Upload.Filter.Dedupe],
|
filters: [],
|
||||||
link_name: false,
|
link_name: false,
|
||||||
proxy_remote: false,
|
proxy_remote: false,
|
||||||
filename_display_max_length: 30,
|
filename_display_max_length: 30,
|
||||||
base_url: nil
|
base_url: nil,
|
||||||
|
allowed_mime_types: ["image", "audio", "video"]
|
||||||
|
|
||||||
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
|
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
|
||||||
|
|
||||||
|
@ -148,18 +149,38 @@ config :logger, :ex_syslogger,
|
||||||
format: "$metadata[$level] $message",
|
format: "$metadata[$level] $message",
|
||||||
metadata: [:request_id]
|
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, %{
|
config :mime, :types, %{
|
||||||
"application/xml" => ["xml"],
|
"application/xml" => ["xml"],
|
||||||
"application/xrd+xml" => ["xrd+xml"],
|
"application/xrd+xml" => ["xrd+xml"],
|
||||||
"application/jrd+json" => ["jrd+json"],
|
"application/jrd+json" => ["jrd+json"],
|
||||||
"application/activity+json" => ["activity+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, %{
|
config :mime, :extensions, %{
|
||||||
"activity+json" => "application/activity+json"
|
"xrd+xml" => "text/plain",
|
||||||
|
"jrd+json" => "text/plain",
|
||||||
|
"activity+json" => "text/plain"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ———————————————————————————————————————————————————————————————
|
||||||
|
|
||||||
config :tesla, :adapter, {Tesla.Adapter.Finch, name: MyFinch}
|
config :tesla, :adapter, {Tesla.Adapter.Finch, name: MyFinch}
|
||||||
|
|
||||||
# Configures http settings, upstream proxy etc.
|
# Configures http settings, upstream proxy etc.
|
||||||
|
@ -406,8 +427,6 @@ config :pleroma, :mrf_object_age,
|
||||||
threshold: 604_800,
|
threshold: 604_800,
|
||||||
actions: [:delist, :strip_followers]
|
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, :mrf_reject_newly_created_account_notes, age: 86_400
|
||||||
|
|
||||||
config :pleroma, :rich_media,
|
config :pleroma, :rich_media,
|
||||||
|
|
|
@ -100,9 +100,22 @@ config :pleroma, :config_description, [
|
||||||
label: "Base URL",
|
label: "Base URL",
|
||||||
type: :string,
|
type: :string,
|
||||||
description:
|
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: [
|
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,
|
group: :pleroma,
|
||||||
key: Pleroma.Emails.Mailer,
|
key: Pleroma.Emails.Mailer,
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
import Config
|
|
||||||
|
|
||||||
config :pleroma, Pleroma.Web.Endpoint,
|
|
||||||
http: [
|
|
||||||
port: String.to_integer(System.get_env("PORT") || "4000"),
|
|
||||||
protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]
|
|
||||||
],
|
|
||||||
protocol: "http",
|
|
||||||
secure_cookie_flag: false,
|
|
||||||
url: [host: System.get_env("APP_HOST"), scheme: "https", port: 443],
|
|
||||||
secret_key_base: "+S+ULgf7+N37c/lc9K66SMphnjQIRGklTu0BRr2vLm2ZzvK0Z6OH/PE77wlUNtvP"
|
|
||||||
|
|
||||||
database_url =
|
|
||||||
System.get_env("DATABASE_URL") ||
|
|
||||||
raise """
|
|
||||||
environment variable DATABASE_URL is missing.
|
|
||||||
For example: ecto://USER:PASS@HOST/DATABASE
|
|
||||||
"""
|
|
||||||
|
|
||||||
config :pleroma, Pleroma.Repo,
|
|
||||||
# ssl: true,
|
|
||||||
url: database_url,
|
|
||||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
|
|
||||||
|
|
||||||
config :pleroma, :instance, name: "#{System.get_env("APP_NAME")} CI Instance"
|
|
|
@ -22,9 +22,12 @@ config :logger, :console,
|
||||||
config :pleroma, :auth, oauth_consumer_strategies: []
|
config :pleroma, :auth, oauth_consumer_strategies: []
|
||||||
|
|
||||||
config :pleroma, Pleroma.Upload,
|
config :pleroma, Pleroma.Upload,
|
||||||
|
base_url: "http://localhost:4001/media/",
|
||||||
filters: [],
|
filters: [],
|
||||||
link_name: false
|
link_name: false
|
||||||
|
|
||||||
|
config :pleroma, :media_proxy, base_url: "http://localhost:4001"
|
||||||
|
|
||||||
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
|
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
|
||||||
|
|
||||||
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test, enabled: true
|
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"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -46,7 +46,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- .:/opt/akkoma
|
- .:/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:
|
#proxy:
|
||||||
# image: caddy:2-alpine
|
# image: caddy:2-alpine
|
||||||
# restart: unless-stopped
|
# 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.)
|
- `--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-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
|
- `--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
|
- `--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
|
- `--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
|
- `--skip-release-env` - skip generation the release environment file
|
||||||
|
|
56
docs/docs/administration/CLI_tasks/security.md
Normal file
56
docs/docs/administration/CLI_tasks/security.md
Normal file
|
@ -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
|
||||||
|
```
|
|
@ -1,12 +1,15 @@
|
||||||
# Akkoma Clients
|
# Akkoma Clients
|
||||||
Note: Additional clients may work, but these are known to work with Akkoma.
|
This is a list of clients that are known to work with Akkoma.
|
||||||
Apps listed here might not support all of Akkoma's features.
|
|
||||||
|
!!! warning
|
||||||
|
**Clients listed here are not officially supported by the Akkoma project.**
|
||||||
|
Some Akkoma features may be unsupported by these clients.
|
||||||
|
|
||||||
## Multiplatform
|
## Multiplatform
|
||||||
### Kaiteki
|
### Kaiteki
|
||||||
- Homepage: <https://kaiteki.app/>
|
- Homepage: <https://kaiteki.app/>
|
||||||
- Source Code: <https://github.com/Kaiteki-Fedi/Kaiteki>
|
- Source Code: <https://github.com/Kaiteki-Fedi/Kaiteki>
|
||||||
- Contact: [@kaiteki@fedi.software](https://fedi.software/@Kaiteki)
|
- Contact: [@kaiteki@social.kaiteki.app](https://social.kaiteki.app/@kaiteki)
|
||||||
- Platforms: Web, Windows, Linux, Android
|
- Platforms: Web, Windows, Linux, Android
|
||||||
- Features: MastoAPI, Supports multiple backends
|
- Features: MastoAPI, Supports multiple backends
|
||||||
|
|
||||||
|
@ -38,12 +41,6 @@ Apps listed here might not support all of Akkoma's features.
|
||||||
- Platforms: Android
|
- Platforms: Android
|
||||||
- Features: MastoAPI, No Streaming, Emoji Reactions, Text Formatting, FE Stickers
|
- Features: MastoAPI, No Streaming, Emoji Reactions, Text Formatting, FE Stickers
|
||||||
|
|
||||||
### Fedi
|
|
||||||
- Homepage: <https://www.fediapp.com/>
|
|
||||||
- Source Code: Proprietary, but gratis
|
|
||||||
- Platforms: iOS, Android
|
|
||||||
- Features: MastoAPI, Pleroma-specific features like Reactions
|
|
||||||
|
|
||||||
### Tusky
|
### Tusky
|
||||||
- Homepage: <https://tuskyapp.github.io/>
|
- Homepage: <https://tuskyapp.github.io/>
|
||||||
- Source Code: <https://github.com/tuskyapp/Tusky>
|
- Source Code: <https://github.com/tuskyapp/Tusky>
|
||||||
|
@ -51,12 +48,18 @@ Apps listed here might not support all of Akkoma's features.
|
||||||
- Platforms: Android
|
- Platforms: Android
|
||||||
- Features: MastoAPI, No Streaming
|
- Features: MastoAPI, No Streaming
|
||||||
|
|
||||||
|
### Subway Tooter
|
||||||
|
- Source Code: <https://github.com/tateisu/SubwayTooter/>
|
||||||
|
- Contact: [@SubwayTooter@mastodon.juggler.jp](https://mastodon.juggler.jp/@SubwayTooter)
|
||||||
|
- Platforms: Android
|
||||||
|
- Features: MastoAPI, Editing, Emoji Reactions (including custom emoji)
|
||||||
|
|
||||||
## Alternative Web Interfaces
|
## Alternative Web Interfaces
|
||||||
### Pinafore
|
### Enafore
|
||||||
- Note: Pinafore is unmaintained (See [the author's original article](https://nolanlawson.com/2023/01/09/retiring-pinafore/) for details)
|
- An actively developed fork of Pinafore with improved Akkoma support
|
||||||
- Homepage: <https://pinafore.social/>
|
- Homepage: <https://enafore.social/>
|
||||||
- Source Code: <https://github.com/nolanlawson/pinafore>
|
- Source Code: <https://github.com/enafore/enafore>
|
||||||
- Contact: [@pinafore@mastodon.technology](https://mastodon.technology/users/pinafore)
|
- Contact: [@enfore@enafore.social](https://meta.enafore.social/@enafore)
|
||||||
- Features: MastoAPI, No Streaming
|
- Features: MastoAPI, No Streaming
|
||||||
|
|
||||||
### Sengi
|
### Sengi
|
||||||
|
|
|
@ -63,6 +63,8 @@ To add configuration to your config file, you can copy it from the base config.
|
||||||
* `local_bubble`: Array of domains representing instances closely related to yours. Used to populate the `bubble` timeline. e.g `["example.com"]`, (default: `[]`)
|
* `local_bubble`: Array of domains representing instances closely related to yours. Used to populate the `bubble` timeline. e.g `["example.com"]`, (default: `[]`)
|
||||||
* `languages`: List of Language Codes used by the instance. This is used to try and set a default language from the frontend. It will try and find the first match between the languages set here and the user's browser languages. It will default to the first language in this setting if there is no match.. (default `["en"]`)
|
* `languages`: List of Language Codes used by the instance. This is used to try and set a default language from the frontend. It will try and find the first match between the languages set here and the user's browser languages. It will default to the first language in this setting if there is no match.. (default `["en"]`)
|
||||||
* `export_prometheus_metrics`: Enable prometheus metrics, served at `/api/v1/akkoma/metrics`, requiring the `admin:metrics` oauth scope.
|
* `export_prometheus_metrics`: Enable prometheus metrics, served at `/api/v1/akkoma/metrics`, requiring the `admin:metrics` oauth scope.
|
||||||
|
* `privileged_staff`: Set to `true` to give moderators access to a few higher responsibility actions.
|
||||||
|
* `federated_timeline_available`: Set to `false` to remove access to the federated timeline for all users.
|
||||||
|
|
||||||
## :database
|
## :database
|
||||||
* `improved_hashtag_timeline`: Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes).
|
* `improved_hashtag_timeline`: Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes).
|
||||||
|
@ -104,31 +106,60 @@ To add configuration to your config file, you can copy it from the base config.
|
||||||
## Message rewrite facility
|
## Message rewrite facility
|
||||||
|
|
||||||
### :mrf
|
### :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`: 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_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`
|
* `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
|
## 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
|
### MRF policies
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
@ -207,7 +238,9 @@ config :pleroma, :mrf_user_allowlist, %{
|
||||||
#### :mrf_steal_emoji
|
#### :mrf_steal_emoji
|
||||||
* `hosts`: List of hosts to steal emojis from
|
* `hosts`: List of hosts to steal emojis from
|
||||||
* `rejected_shortcodes`: Regex-list of shortcodes to reject
|
* `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
|
#### :mrf_activity_expiration
|
||||||
|
|
||||||
|
@ -223,14 +256,24 @@ Notes:
|
||||||
- The hashtags in the configuration do not have a leading `#`.
|
- 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
|
- This MRF Policy is always enabled, if you want to disable it you have to set empty lists
|
||||||
|
|
||||||
### :activitypub
|
#### :mrf_reject_newly_created_account_notes
|
||||||
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
|
After initially encountering an user, all their posts
|
||||||
* `outgoing_blocks`: Whether to federate blocks to other instances
|
will be rejected for the configured time (in seconds).
|
||||||
* `blockers_visible`: Whether a user can see the posts of users who blocked them
|
Only drops posts. Follows, reposts, etc. are not affected.
|
||||||
* `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
|
* `age`: Time below which to reject (in seconds)
|
||||||
* `authorized_fetch_mode`: Require HTTP signatures for AP fetches
|
|
||||||
* `max_collection_objects`: The maximum number of objects to fetch from a remote AP collection.
|
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
|
## Pleroma.User
|
||||||
|
|
||||||
|
@ -357,7 +400,8 @@ This section describe PWA manifest instance-specific values. Currently this opti
|
||||||
## :media_proxy
|
## :media_proxy
|
||||||
|
|
||||||
* `enabled`: Enables proxying of remote media to the instance’s 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)]`.
|
* `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`)
|
* `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:
|
* `invalidation`: options for remove media from cache after delete object:
|
||||||
|
@ -558,8 +602,9 @@ the source code is here: [kocaptcha](https://github.com/koto-bank/kocaptcha). Th
|
||||||
|
|
||||||
* `uploader`: Which one of the [uploaders](#uploaders) to use.
|
* `uploader`: Which one of the [uploaders](#uploaders) to use.
|
||||||
* `filters`: List of [upload filters](#upload-filters) 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`
|
* `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. Useful when you want to host the media files via another domain or are using a 3rd party S3 provider.
|
* `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_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.
|
* `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.
|
* `filename_display_max_length`: Set max length of a filename to display. 0 = no limit. Default: 30.
|
||||||
|
@ -599,20 +644,29 @@ config :ex_aws, :s3,
|
||||||
|
|
||||||
### Upload filters
|
### 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
|
#### Pleroma.Upload.Filter.AnonymizeFilename
|
||||||
|
|
||||||
This filter replaces the filename (not the path) of an upload. For complete obfuscation, add
|
This filter replaces the declared filename (not the path) of an upload.
|
||||||
`Pleroma.Upload.Filter.Dedupe` before AnonymizeFilename.
|
|
||||||
|
|
||||||
* `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}`.
|
* `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.
|
No specific configuration.
|
||||||
|
|
||||||
|
|
|
@ -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))
|
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`
|
### `:http_security`
|
||||||
|
|
||||||
> Recommended value: `true`
|
> Recommended value: `true`
|
||||||
|
|
|
@ -6,29 +6,18 @@ With the `mediaproxy` function you can use nginx to cache this content, so users
|
||||||
|
|
||||||
## Activate it
|
## Activate it
|
||||||
|
|
||||||
* Edit your nginx config and add the following location:
|
* Set up a subdomain for the proxy with its nginx config on the same machine
|
||||||
```
|
* Edit the nginx config for the upload/MediaProxy subdomain to point to the subdomain that has been set up
|
||||||
location /proxy {
|
|
||||||
proxy_cache akkoma_media_cache;
|
|
||||||
proxy_cache_lock on;
|
|
||||||
proxy_pass http://localhost:4000;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Also add the following on top of the configuration, outside of the `server` block:
|
|
||||||
```
|
|
||||||
proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=10g inactive=720m use_temp_path=off;
|
|
||||||
```
|
|
||||||
If you came here from one of the installation guides, take a look at the example configuration `/installation/nginx/akkoma.nginx`, where this part is already included.
|
|
||||||
|
|
||||||
* Append the following to your `prod.secret.exs` or `dev.secret.exs` (depends on which mode your instance is running):
|
* Append the following to your `prod.secret.exs` or `dev.secret.exs` (depends on which mode your instance is running):
|
||||||
```
|
```elixir
|
||||||
|
# Replace media.example.td with the subdomain you set up earlier
|
||||||
config :pleroma, :media_proxy,
|
config :pleroma, :media_proxy,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
proxy_opts: [
|
proxy_opts: [
|
||||||
redirect_on_failure: true
|
redirect_on_failure: true
|
||||||
]
|
],
|
||||||
#base_url: "https://cache.akkoma.social"
|
base_url: "https://media.example.tld"
|
||||||
```
|
```
|
||||||
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
|
* Restart nginx and Akkoma
|
||||||
|
|
|
@ -130,59 +130,26 @@ config :pleroma, :http_security,
|
||||||
enabled: false
|
enabled: false
|
||||||
```
|
```
|
||||||
|
|
||||||
Use this as the Nginx config:
|
In the Nginx config, add the following into the `location /` block:
|
||||||
```
|
```nginx
|
||||||
proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=10g inactive=720m use_temp_path=off;
|
|
||||||
# The above already exists in a clearnet instance's config.
|
|
||||||
# If not, add it.
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 127.0.0.1:14447;
|
|
||||||
server_name youri2paddress;
|
|
||||||
|
|
||||||
# Comment to enable logs
|
|
||||||
access_log /dev/null;
|
|
||||||
error_log /dev/null;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
client_max_body_size 16m;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
|
|
||||||
add_header X-XSS-Protection "0";
|
add_header X-XSS-Protection "0";
|
||||||
add_header X-Permitted-Cross-Domain-Policies none;
|
add_header X-Permitted-Cross-Domain-Policies none;
|
||||||
add_header X-Frame-Options DENY;
|
add_header X-Frame-Options DENY;
|
||||||
add_header X-Content-Type-Options nosniff;
|
add_header X-Content-Type-Options nosniff;
|
||||||
add_header Referrer-Policy same-origin;
|
add_header Referrer-Policy same-origin;
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
|
|
||||||
proxy_pass http://localhost:4000;
|
|
||||||
|
|
||||||
client_max_body_size 16m;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /proxy {
|
|
||||||
proxy_cache akkoma_media_cache;
|
|
||||||
proxy_cache_lock on;
|
|
||||||
proxy_ignore_client_abort on;
|
|
||||||
proxy_pass http://localhost:4000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
reload Nginx:
|
|
||||||
|
Change the `listen` directive to the following:
|
||||||
|
```nginx
|
||||||
|
listen 127.0.0.1:14447;
|
||||||
```
|
```
|
||||||
systemctl stop i2pd.service --no-block
|
|
||||||
systemctl start i2pd.service
|
Set `server_name` to your i2p address.
|
||||||
|
|
||||||
|
Reload Nginx:
|
||||||
|
```
|
||||||
|
systemctl restart i2pd.service --no-block
|
||||||
|
systemctl reload nginx.service
|
||||||
```
|
```
|
||||||
*Notice:* The stop command initiates a graceful shutdown process, i2pd stops after finishing to route transit tunnels (maximum 10 minutes).
|
*Notice:* The stop command initiates a graceful shutdown process, i2pd stops after finishing to route transit tunnels (maximum 10 minutes).
|
||||||
|
|
||||||
|
|
|
@ -74,56 +74,23 @@ config :pleroma, :http_security,
|
||||||
enabled: false
|
enabled: false
|
||||||
```
|
```
|
||||||
|
|
||||||
Use this as the Nginx config:
|
In the Nginx config, add the following into the `location /` block:
|
||||||
```
|
```nginx
|
||||||
proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=10g inactive=720m use_temp_path=off;
|
|
||||||
# The above already exists in a clearnet instance's config.
|
|
||||||
# If not, add it.
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 127.0.0.1:8099;
|
|
||||||
server_name youronionaddress;
|
|
||||||
|
|
||||||
# Comment to enable logs
|
|
||||||
access_log /dev/null;
|
|
||||||
error_log /dev/null;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
client_max_body_size 16m;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
|
|
||||||
add_header X-XSS-Protection "0";
|
add_header X-XSS-Protection "0";
|
||||||
add_header X-Permitted-Cross-Domain-Policies none;
|
add_header X-Permitted-Cross-Domain-Policies none;
|
||||||
add_header X-Frame-Options DENY;
|
add_header X-Frame-Options DENY;
|
||||||
add_header X-Content-Type-Options nosniff;
|
add_header X-Content-Type-Options nosniff;
|
||||||
add_header Referrer-Policy same-origin;
|
add_header Referrer-Policy same-origin;
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
|
|
||||||
proxy_pass http://localhost:4000;
|
|
||||||
|
|
||||||
client_max_body_size 16m;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /proxy {
|
|
||||||
proxy_cache akkoma_media_cache;
|
|
||||||
proxy_cache_lock on;
|
|
||||||
proxy_ignore_client_abort on;
|
|
||||||
proxy_pass http://localhost:4000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
reload Nginx:
|
|
||||||
|
Change the `listen` directive to the following:
|
||||||
|
```nginx
|
||||||
|
listen 127.0.0.1:8099;
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the `server_name` to your onion address.
|
||||||
|
|
||||||
|
Reload Nginx:
|
||||||
```
|
```
|
||||||
systemctl reload nginx
|
systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
|
@ -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`
|
`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.
|
Check your OS documentation to adopt a similar strategy on other platforms.
|
||||||
|
|
||||||
### Virtual Machine and/or few CPU cores
|
### 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:**
|
**vm.args:**
|
||||||
|
|
||||||
|
@ -39,6 +42,8 @@ Disable the busy-waiting. This should generally only be done if you're on a plat
|
||||||
+sbwtdio none
|
+sbwtdio none
|
||||||
```
|
```
|
||||||
|
|
||||||
|
These settings are enabled by default for OTP releases
|
||||||
|
|
||||||
### Dedicated Hardware
|
### 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.
|
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.
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
The following endpoints are additionally present into our actors.
|
The following endpoints are additionally present into our actors.
|
||||||
|
|
||||||
- `oauthRegistrationEndpoint` (`http://litepub.social/ns#oauthRegistrationEndpoint`)
|
- `oauthRegistrationEndpoint` (`http://litepub.social/ns#oauthRegistrationEndpoint`)
|
||||||
- `uploadMedia` (`https://www.w3.org/ns/activitystreams#uploadMedia`)
|
|
||||||
|
|
||||||
### oauthRegistrationEndpoint
|
### oauthRegistrationEndpoint
|
||||||
|
|
||||||
|
@ -12,6 +11,279 @@ Points to MastodonAPI `/api/v1/apps` for now.
|
||||||
|
|
||||||
See <https://docs.joinmastodon.org/methods/apps/>
|
See <https://docs.joinmastodon.org/methods/apps/>
|
||||||
|
|
||||||
|
## Emoji reactions
|
||||||
|
|
||||||
|
Emoji reactions are implemented as a new activity type `EmojiReact`.
|
||||||
|
A single user is allowed to react multiple times with different emoji to the
|
||||||
|
same post. However, they may only react at most once with the same emoji.
|
||||||
|
Repeated reaction from the same user with the same emoji are to be ignored.
|
||||||
|
Emoji reactions are also distinct from `Like` activities and a user may both
|
||||||
|
`Like` and react to a post.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Misskey also supports emoji reactions, but the implementations differs.
|
||||||
|
It equates likes and reactions and only allows a single reaction per post.
|
||||||
|
|
||||||
|
The emoji is placed in the `content` field of the activity
|
||||||
|
and the `object` property points to the note reacting to.
|
||||||
|
|
||||||
|
Emoji can either be any Unicode emoji sequence or a custom emoji.
|
||||||
|
The latter must place their shortcode, including enclosing colons,
|
||||||
|
into `content` and put the emoji object inside the `tag` property.
|
||||||
|
The `tag` property MAY be omitted for Unicode emoji.
|
||||||
|
|
||||||
|
An example reaction with a Unicode emoji:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://example.org/schemas/litepub-0.1.jsonld",
|
||||||
|
{
|
||||||
|
"@language": "und"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "EmojiReact",
|
||||||
|
"id": "https://example.org/activities/23143872a0346141",
|
||||||
|
"actor": "https://example.org/users/akko",
|
||||||
|
"nickname": "akko",
|
||||||
|
"to": ["https://remote.example/users/diana", "https://example.org/users/akko/followers"],
|
||||||
|
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"content": "🧡",
|
||||||
|
"object": "https://remote.example/objects/9f0e93499d8314a9"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
An example reaction with a custom emoji:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://example.org/schemas/litepub-0.1.jsonld",
|
||||||
|
{
|
||||||
|
"@language": "und"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "EmojiReact",
|
||||||
|
"id": "https://example.org/activities/d75586dec0541650",
|
||||||
|
"actor": "https://example.org/users/akko",
|
||||||
|
"nickname": "akko",
|
||||||
|
"to": ["https://remote.example/users/diana", "https://example.org/users/akko/followers"],
|
||||||
|
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"content": ":mouse:",
|
||||||
|
"object": "https://remote.example/objects/9f0e93499d8314a9",
|
||||||
|
"tag": [{
|
||||||
|
"type": "Emoji",
|
||||||
|
"id": null,
|
||||||
|
"name": "mouse",
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"url": "https://example.org/emoji/mouse/mouse.png"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Although an emoji reaction can only contain a single emoji,
|
||||||
|
for compatibility with older versions of Pleroma and Akkoma,
|
||||||
|
it is recommended to wrap the emoji object in a single-element array.
|
||||||
|
|
||||||
|
When reacting with a remote custom emoji do not include the remote domain in `content`’s shortcode
|
||||||
|
*(unlike in our REST API which needs the domain)*:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://example.org/schemas/litepub-0.1.jsonld",
|
||||||
|
{
|
||||||
|
"@language": "und"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "EmojiReact",
|
||||||
|
"id": "https://example.org/activities/7993dcae98d8d5ec",
|
||||||
|
"actor": "https://example.org/users/akko",
|
||||||
|
"nickname": "akko",
|
||||||
|
"to": ["https://remote.example/users/diana", "https://example.org/users/akko/followers"],
|
||||||
|
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"content": ":hug:",
|
||||||
|
"object": "https://remote.example/objects/9f0e93499d8314a9",
|
||||||
|
"tag": [{
|
||||||
|
"type": "Emoji",
|
||||||
|
"id": "https://other.example/emojis/hug",
|
||||||
|
"name": "hug",
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"url": "https://other.example/files/b71cea432b3fad67.webp"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Emoji reactions can be retracted using a standard `Undo` activity:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"http://example.org/schemas/litepub-0.1.jsonld",
|
||||||
|
{
|
||||||
|
"@language": "und"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Undo",
|
||||||
|
"id": "http://example.org/activities/4685792e-efb6-4309-b508-ae4f355dd695",
|
||||||
|
"actor": "https://example.org/users/akko",
|
||||||
|
"to": ["https://remote.example/users/diana", "https://example.org/users/akko/followers"],
|
||||||
|
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"object": "https://example.org/activities/23143872a0346141"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## User profile backgrounds
|
||||||
|
|
||||||
|
Akkoma federates user profile backgrounds the same way as Sharkey.
|
||||||
|
|
||||||
|
An actors ActivityPub representation contains an additional
|
||||||
|
`backgroundUrl` property containing an `Image` object. This property
|
||||||
|
belongs to the `"sharkey": "https://joinsharkey.org/ns#"` namespace.
|
||||||
|
|
||||||
|
## Quote Posts
|
||||||
|
|
||||||
|
Akkoma allows referencing a single other note as a quote,
|
||||||
|
which will be prominently displayed in the interface.
|
||||||
|
|
||||||
|
The quoted post is referenced by its ActivityPub id in the `quoteUri` property.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Old Misskey only understood and modern Misskey still prefers
|
||||||
|
the `_misskey_quote` property for this. Similar some other older
|
||||||
|
software used `quoteUrl` or `quoteURL`.
|
||||||
|
All current implementations with quote support understand `quoteUri`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://example.org/schemas/litepub-0.1.jsonld",
|
||||||
|
{
|
||||||
|
"@language": "und"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Note",
|
||||||
|
"id": "https://example.org/activities/85717e587f95d5c0",
|
||||||
|
"actor": "https://example.org/users/akko",
|
||||||
|
"to": ["https://remote.example/users/diana", "https://example.org/users/akko/followers"],
|
||||||
|
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"context": "https://example.org/contexts/1",
|
||||||
|
"content": "Look at that!",
|
||||||
|
"quoteUri": "http://remote.example/status/85717e587f95d5c0",
|
||||||
|
"contentMap": {
|
||||||
|
"en": "Look at that!"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"content": "Look at that!",
|
||||||
|
"mediaType": "text/plain"
|
||||||
|
},
|
||||||
|
"published": "2024-04-06T23:40:28Z",
|
||||||
|
"updated": "2024-04-06T23:40:28Z",
|
||||||
|
"attachemnt": [],
|
||||||
|
"tag": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Threads
|
||||||
|
|
||||||
|
Akkoma assigns all posts of the same thread the same `context`. This is a
|
||||||
|
standard ActivityPub property but its meaning is left vague. Akkoma will
|
||||||
|
always treat posts with identical `context` as part of the same thread.
|
||||||
|
|
||||||
|
`context` must not be assumed to hold any meaning or be dereferencable.
|
||||||
|
|
||||||
|
Incoming posts without `context` will be assigned a new context.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Mastodon uses the non-standard `conversation` property for the same purpose
|
||||||
|
*(named after an older OStatus property)*. For incoming posts without
|
||||||
|
`context` but with `converstions` Akkoma will use the value from
|
||||||
|
`conversations` to fill in `context`.
|
||||||
|
For outgoing posts Akkoma will duplicate the context into `conversation`.
|
||||||
|
|
||||||
|
## Post Source
|
||||||
|
|
||||||
|
Unlike Mastodon, Akkoma supports drafting posts in multiple source formats
|
||||||
|
besides plaintext, like Markdown or MFM. The original input is preserved
|
||||||
|
in the standard ActivityPub `source` property *(not supported by Mastodon)*.
|
||||||
|
Still, `content` will always be present and contain the prerendered HTML form.
|
||||||
|
|
||||||
|
Supported `mediaType` include:
|
||||||
|
- `text/plain`
|
||||||
|
- `text/markdown`
|
||||||
|
- `text/bbcode`
|
||||||
|
- `text/x.misskeymarkdown`
|
||||||
|
|
||||||
|
## Post Language
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
This is also supported in and compatible with Mastodon, but since
|
||||||
|
joinmastodon.org doesn’t document it yet it is included here.
|
||||||
|
[GoToSocial](https://docs.gotosocial.org/en/latest/federation/federating_with_gotosocial/#content-contentmap-and-language)
|
||||||
|
has a more refined version of this which can correctly deal with multiple language entries.
|
||||||
|
|
||||||
|
A post can indicate its language by including a `contentMap` object
|
||||||
|
which contains a sub key named after the language’s ISO 639-1 code
|
||||||
|
and it’s content identical to the post’s `content` field.
|
||||||
|
|
||||||
|
Currently Akkoma, just like Mastodon, only properly supports a single language entry,
|
||||||
|
in case of multiple entries a random language will be picked.
|
||||||
|
Furthermore, Akkoma currently only reads the `content` field
|
||||||
|
and never the value from `contentMap`.
|
||||||
|
|
||||||
|
## Local post scope
|
||||||
|
|
||||||
|
Post using this scope will never federate to other servers
|
||||||
|
but for the sake of completeness it is listed here.
|
||||||
|
|
||||||
|
In addition to the usual scopes *(public, unlisted, followers-only, direct)*
|
||||||
|
Akkoma supports an “unlisted” post scope. Such posts will not federate to
|
||||||
|
other instances and only be shown to logged-in users on the same instance.
|
||||||
|
It is included into the local timeline.
|
||||||
|
This may be useful to discuss or announce instance-specific policies and topics.
|
||||||
|
|
||||||
|
A post is addressed to the local scope by including `<base url of instance>/#Public`
|
||||||
|
in its `to` field. E.g. if the instance is on `https://example.org` it would use
|
||||||
|
`https://example.org/#Public`.
|
||||||
|
|
||||||
|
An implementation creating a new post MUST NOT address both the local and
|
||||||
|
general public scope `as:Public` at the same time. A post addressing the local
|
||||||
|
scope MUST NOT be sent to other instances or be possible to fetch by other
|
||||||
|
instances regardless of potential other listed addressees.
|
||||||
|
|
||||||
|
When receiving a remote post addressing both the public scope and what appears
|
||||||
|
to be a local-scope identifier, the post SHOULD be treated without assigning any
|
||||||
|
special meaning to the potential local-scope identifier.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Misskey-derivatives have a similar concept of non-federated posts,
|
||||||
|
however those are also shown publicly on the local web interface
|
||||||
|
and are thus visible to non-members.
|
||||||
|
|
||||||
|
## List post scope
|
||||||
|
|
||||||
|
Messages originally addressed to a custom list will contain
|
||||||
|
a `listMessage` field with an unresolvable pseudo ActivityPub id.
|
||||||
|
|
||||||
|
# Deprecated and Removed Extensions
|
||||||
|
|
||||||
|
The following extensions were used in the past but have been dropped.
|
||||||
|
Documentation is retained here as a reference and since old objects might
|
||||||
|
still contains related fields.
|
||||||
|
|
||||||
|
## Actor endpoints
|
||||||
|
|
||||||
|
The following endpoints used to be present:
|
||||||
|
|
||||||
|
- `uploadMedia` (`https://www.w3.org/ns/activitystreams#uploadMedia`)
|
||||||
|
|
||||||
### uploadMedia
|
### uploadMedia
|
||||||
|
|
||||||
Inspired by <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>, it is part of the ActivityStreams namespace because it used to be part of the ActivityPub specification and got removed from it.
|
Inspired by <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>, it is part of the ActivityStreams namespace because it used to be part of the ActivityPub specification and got removed from it.
|
||||||
|
@ -20,9 +292,8 @@ Content-Type: multipart/form-data
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- (required) `file`: The file being uploaded
|
- (required) `file`: The file being uploaded
|
||||||
- (optionnal) `description`: A plain-text description of the media, for accessibility purposes.
|
- (optional) `description`: A plain-text description of the media, for accessibility purposes.
|
||||||
|
|
||||||
Response: HTTP 201 Created with the object into the body, no `Location` header provided as it doesn't have an `id`
|
Response: HTTP 201 Created with the object into the body, no `Location` header provided as it doesn't have an `id`
|
||||||
|
|
||||||
The object given in the reponse should then be inserted into an Object's `attachment` field.
|
The object given in the response should then be inserted into an Object's `attachment` field.
|
||||||
|
|
||||||
|
|
141
docs/docs/development/nodeinfo_extensions.md
Normal file
141
docs/docs/development/nodeinfo_extensions.md
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
# Nodeinfo Extensions
|
||||||
|
|
||||||
|
Akkoma currently implements version 2.0 and 2.1 of nodeinfo spec,
|
||||||
|
but provides the following additional fields.
|
||||||
|
|
||||||
|
## metadata
|
||||||
|
|
||||||
|
The spec leaves the content of `metadata` up to implementations
|
||||||
|
and indeed Akkoma adds many fields here apart from the commonly
|
||||||
|
found `nodeName` and `nodeDescription` fields.
|
||||||
|
|
||||||
|
### accountActivationRequired
|
||||||
|
Whether or not users need to confirm their email before completing registration.
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Not to be confused with account approval, where each registration needs to
|
||||||
|
be manually approved by an admin. Account approval has no nodeinfo entry.
|
||||||
|
|
||||||
|
### features
|
||||||
|
|
||||||
|
Array of strings denoting supported server features. E.g. a server supporting
|
||||||
|
quote posts should include a `"quote_posting"` entry here.
|
||||||
|
|
||||||
|
A non-exhaustive list of possible features:
|
||||||
|
- `polls`
|
||||||
|
- `quote_posting`
|
||||||
|
- `editing`
|
||||||
|
- `bubble_timeline`
|
||||||
|
- `pleroma_emoji_reactions` *(Unicode emoji)*
|
||||||
|
- `custom_emoji_reactions`
|
||||||
|
- `akkoma_api`
|
||||||
|
- `akkoma:machine_translation`
|
||||||
|
- `mastodon_api`
|
||||||
|
- `pleroma_api`
|
||||||
|
|
||||||
|
### federatedTimelineAvailable
|
||||||
|
Whether or not the “federated timeline”, i.e. a timeline containing posts from
|
||||||
|
the entire known network, is made available.
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
### federation
|
||||||
|
This section is optional and can contain various custom keys describing federation policies.
|
||||||
|
The following are required to be presented:
|
||||||
|
- `enabled` *(boolean)* whether the server federates at all
|
||||||
|
|
||||||
|
A non-exhaustive list of optional keys:
|
||||||
|
- `exclusions` *(boolean)* whether some federation policies are withheld
|
||||||
|
- `mrf_simple` *(object)* describes how the Simple MRF policy is configured
|
||||||
|
|
||||||
|
### fieldsLimits
|
||||||
|
A JSON object documenting restriction for user account info fields.
|
||||||
|
All properties are integers.
|
||||||
|
|
||||||
|
- `maxFields` maximum number of account info fields local users can create
|
||||||
|
- `maxRemoteFields` maximum number of account info fields remote users can have
|
||||||
|
before the user gets rejected or fields truncated
|
||||||
|
- `nameLength` maximum length of a field’s name
|
||||||
|
- `valueLength` maximum length of a field’s value
|
||||||
|
|
||||||
|
### invitesEnabled
|
||||||
|
Whether or not signing up via invite codes is possible.
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
### localBubbleInstances
|
||||||
|
Array of domains (as strings) of other instances chosen
|
||||||
|
by the admin which are shown in the bubble timeline.
|
||||||
|
|
||||||
|
### mailerEnabled
|
||||||
|
Whether or not the instance can send out emails.
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
### nodeDescription
|
||||||
|
Human-friendly description of this instance
|
||||||
|
*(string)*
|
||||||
|
|
||||||
|
### nodeName
|
||||||
|
Human-friendly name of this instance
|
||||||
|
*(string)*
|
||||||
|
|
||||||
|
### pollLimits
|
||||||
|
JSON object containing limits for polls created by local users.
|
||||||
|
All values are integers.
|
||||||
|
- `max_options` maximum number of poll options
|
||||||
|
- `max_option_chars` maximum characters per poll option
|
||||||
|
- `min_expiration` minimum time in seconds a poll must be open for
|
||||||
|
- `max_expiration` maximum time a poll is allowed to be open for
|
||||||
|
|
||||||
|
### postFormats
|
||||||
|
Array of strings containing media types for supported post source formats.
|
||||||
|
A non-exhaustive list of possible values:
|
||||||
|
- `text/plain`
|
||||||
|
- `text/markdown`
|
||||||
|
- `text/bbcode`
|
||||||
|
- `text/x.misskeymarkdown`
|
||||||
|
|
||||||
|
### private
|
||||||
|
Whether or not unauthenticated API access is permitted.
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
### privilegedStaff
|
||||||
|
Whether or not moderators are trusted to perform some
|
||||||
|
additional tasks like e.g. issuing password reset emails.
|
||||||
|
|
||||||
|
### publicTimelineVisibility
|
||||||
|
JSON object containing boolean-valued keys reporting
|
||||||
|
if a given timeline can be viewed without login.
|
||||||
|
- `local`
|
||||||
|
- `federated`
|
||||||
|
- `bubble`
|
||||||
|
|
||||||
|
### restrictedNicknames
|
||||||
|
Array of strings listing nicknames forbidden to be used during signup.
|
||||||
|
|
||||||
|
### skipThreadContainment
|
||||||
|
Whether broken threads are filtered out
|
||||||
|
*(boolean)*
|
||||||
|
|
||||||
|
### staffAccounts
|
||||||
|
Array containing ActivityPub IDs of local accounts
|
||||||
|
with some form of elevated privilege on the instance.
|
||||||
|
|
||||||
|
### suggestions
|
||||||
|
JSON object containing info on whether the interaction-based
|
||||||
|
Mastodon `/api/v1/suggestions` feature is enabled and optionally
|
||||||
|
additional implementation-defined fields with more details
|
||||||
|
on e.g. how suggested users are selected.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
This has no relation to the newer /api/v2/suggestions API
|
||||||
|
which also (or exclusively) contains staff-curated entries.
|
||||||
|
|
||||||
|
- `enabled` *(boolean)* whether or not user recommendations are enabled
|
||||||
|
|
||||||
|
### uploadLimits
|
||||||
|
JSON object documenting various upload-related size limits.
|
||||||
|
All values are integers and in bytes.
|
||||||
|
- `avatar` maximum size of uploaded user avatars
|
||||||
|
- `banner` maximum size of uploaded user profile banners
|
||||||
|
- `background` maximum size of uploaded user profile backgrounds
|
||||||
|
- `general` maximum size for all other kinds of uploads
|
|
@ -145,47 +145,13 @@ If you want to open your newly installed instance to the world, you should run n
|
||||||
doas apk add nginx
|
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
|
* Copy the example nginx configuration to the nginx folder
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
doas cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/conf.d/akkoma.conf
|
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).
|
* 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).
|
||||||
|
|
||||||
```
|
|
||||||
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;
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Enable and start nginx:
|
* Enable and start nginx:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@ -193,10 +159,37 @@ doas rc-update add nginx
|
||||||
doas rc-service nginx start
|
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
|
```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
|
#### 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
|
sudo pacman -S nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
* Create directories for available and enabled sites:
|
* Copy the example nginx configuration:
|
||||||
|
|
||||||
```shell
|
```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
|
```shell
|
||||||
include sites-enabled/*;
|
sudo systemctl enable --now nginx.service
|
||||||
```
|
```
|
||||||
|
|
||||||
* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it:
|
* 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
|
```shell
|
||||||
sudo mkdir -p /var/lib/letsencrypt/
|
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`.
|
||||||
|
|
||||||
---
|
To make sure renewals work, enable the appropriate systemd timer:
|
||||||
|
|
||||||
* Copy the example nginx configuration and activate it:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo cp /opt/akkoma/installation/nginx/akkoma.nginx /etc/nginx/sites-available/akkoma.nginx
|
sudo systemctl enable --now certbot-renew.timer
|
||||||
sudo ln -s /etc/nginx/sites-available/akkoma.nginx /etc/nginx/sites-enabled/akkoma.nginx
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* Before starting nginx edit the configuration and change it to your needs (e.g. change servername, change cert paths)
|
Certificate renewal should be handled automatically by Certbot from now on.
|
||||||
* 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/
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Other webserver/proxies
|
#### 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
|
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:
|
* Copy the example nginx configuration and activate it:
|
||||||
|
|
||||||
```shell
|
```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
|
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
|
```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
|
#### Other webserver/proxies
|
||||||
|
|
||||||
You can find example configurations for them in `/opt/akkoma/installation/`.
|
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.
|
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.
|
then run `docker compose up -d` again.
|
||||||
|
|
||||||
#### Running a reverse proxy on the host
|
#### Running a reverse proxy on the host
|
||||||
|
@ -155,6 +174,12 @@ git pull
|
||||||
docker compose restart akkoma db
|
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
|
#### Further reading
|
||||||
|
|
||||||
{! installation/further_reading.include !}
|
{! 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
|
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:
|
* Copy the example nginx configuration and activate it:
|
||||||
|
|
||||||
```shell
|
```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
|
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
|
```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
|
#### Other webserver/proxies
|
||||||
|
|
||||||
You can find example configurations for them in `/opt/akkoma/installation/`.
|
You can find example configurations for them in `/opt/akkoma/installation/`.
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
## Required dependencies
|
## Required dependencies
|
||||||
|
|
||||||
* PostgreSQL 9.6+
|
* PostgreSQL 9.6+
|
||||||
* Elixir 1.14+
|
* Elixir 1.14+ (currently tested up to 1.16)
|
||||||
* Erlang OTP 25+
|
* Erlang OTP 25+ (currently tested up to OTP26)
|
||||||
* git
|
* git
|
||||||
* file / libmagic
|
* file / libmagic
|
||||||
* gcc (clang might also work)
|
* 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/*;
|
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:
|
* Copy the example nginx configuration and activate it:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@ -237,9 +218,24 @@ Pay special attention to the line that begins with `ssl_ecdh_curve`. It is stong
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# rc-update add nginx default
|
# 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.
|
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.
|
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
|
You'll need to update the backend, then possibly the frontend, depending
|
||||||
on your setup.
|
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
|
## From Source
|
||||||
|
|
||||||
If you're running the source Akkoma install, you'll need to set the
|
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)
|
# to run "git merge stable" instead (or develop if you want)
|
||||||
```
|
```
|
||||||
|
|
||||||
### WARNING - Migrating from Pleroma Develop
|
And compile as usual.
|
||||||
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 -n5
|
|
||||||
```
|
|
||||||
|
|
||||||
Then compile, migrate and restart as usual.
|
|
||||||
|
|
||||||
## From OTP
|
## 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]
|
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 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
|
```bash
|
||||||
./bin/pleroma_ctl update --branch stable
|
./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
|
## Frontend changes
|
||||||
|
|
||||||
Akkoma comes with a few frontend changes as well as backend ones,
|
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.
|
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.
|
`ImageMagick` is a set of tools to create, edit, compose, or convert bitmap images.
|
||||||
|
|
||||||
It is required for the following Akkoma features:
|
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`)
|
* Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`)
|
||||||
|
|
||||||
## `ffmpeg`
|
## `ffmpeg`
|
||||||
|
@ -29,4 +29,5 @@ It is required for the following Akkoma features:
|
||||||
`exiftool` is media files metadata reader/writer.
|
`exiftool` is media files metadata reader/writer.
|
||||||
|
|
||||||
It is required for the following Akkoma features:
|
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`)
|
||||||
|
|
|
@ -9,7 +9,7 @@ This guide covers a installation using an OTP release. To install Akkoma from so
|
||||||
* For installing OTP releases on RedHat-based distros like Fedora and Centos Stream, please follow [this guide](./otp_redhat_en.md) instead.
|
* 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
|
* 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.
|
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,11 +176,6 @@ su akkoma -s $SHELL -lc "./bin/pleroma stop"
|
||||||
|
|
||||||
### Setting up nginx and getting Let's Encrypt SSL certificaties
|
### 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
|
#### Copy Akkoma nginx configuration to the nginx folder
|
||||||
|
|
||||||
The location of nginx configs is dependent on the distro
|
The location of nginx configs is dependent on the distro
|
||||||
|
@ -209,6 +204,14 @@ $EDITOR path-to-nginx-config
|
||||||
# Verify that the config is valid
|
# Verify that the config is valid
|
||||||
nginx -t
|
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
|
#### Start nginx
|
||||||
|
|
||||||
=== "Alpine"
|
=== "Alpine"
|
||||||
|
@ -252,32 +255,19 @@ If everything worked, you should see Akkoma-FE when visiting your domain. If tha
|
||||||
## Post installation
|
## Post installation
|
||||||
|
|
||||||
### Setting up auto-renew of the Let's Encrypt certificate
|
### 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"
|
=== "Alpine"
|
||||||
```
|
```
|
||||||
# Restart nginx
|
|
||||||
rc-service nginx restart
|
|
||||||
|
|
||||||
# Start the cron daemon and make it start on boot
|
# Start the cron daemon and make it start on boot
|
||||||
rc-service crond start
|
rc-service crond start
|
||||||
rc-update add crond
|
rc-update add crond
|
||||||
|
|
||||||
# Ensure the webroot menthod and post hook is working
|
# 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
|
# Add it to the daily cron
|
||||||
echo '#!/bin/sh
|
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
|
' > /etc/periodic/daily/renew-akkoma-cert
|
||||||
chmod +x /etc/periodic/daily/renew-akkoma-cert
|
chmod +x /etc/periodic/daily/renew-akkoma-cert
|
||||||
|
|
||||||
|
@ -286,22 +276,7 @@ nginx -t
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "Debian/Ubuntu"
|
=== "Debian/Ubuntu"
|
||||||
```
|
This should be automatically enabled with the `certbot-renew.timer` systemd unit.
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create your first user and set as admin
|
## Create your first user and set as admin
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -82,6 +82,7 @@ Other than things bundled in the OTP release Akkoma depends on:
|
||||||
* PostgreSQL (also utilizes extensions in postgresql-contrib)
|
* PostgreSQL (also utilizes extensions in postgresql-contrib)
|
||||||
* nginx (could be swapped with another reverse proxy but this guide covers only it)
|
* 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)
|
* 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
|
* libmagic/file
|
||||||
|
|
||||||
First, update your system, if not already done:
|
First, update your system, if not already done:
|
||||||
|
@ -169,12 +170,6 @@ sudo -Hu akkoma ./bin/pleroma stop
|
||||||
|
|
||||||
### Setting up nginx and getting Let's Encrypt SSL certificaties
|
### 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
|
#### Copy Akkoma nginx configuration to the nginx folder
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@ -195,8 +190,15 @@ sudo nginx -t
|
||||||
sudo systemctl start nginx
|
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
|
### Setting up a system service
|
||||||
|
|
||||||
|
@ -239,19 +241,11 @@ sudo nginx -t
|
||||||
# Restart nginx
|
# Restart nginx
|
||||||
sudo systemctl restart nginx
|
sudo systemctl restart nginx
|
||||||
|
|
||||||
# Ensure the webroot menthod and post hook is working
|
# Test that renewals work properly
|
||||||
sudo certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --dry-run --post-hook 'systemctl reload nginx'
|
sudo 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 "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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
## Create your first user and set as admin
|
||||||
```shell
|
```shell
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
elixir_version=1.14.3
|
|
||||||
erlang_version=25.3
|
|
|
@ -60,7 +60,7 @@ ServerTokens Prod
|
||||||
Include /etc/letsencrypt/options-ssl-apache.conf
|
Include /etc/letsencrypt/options-ssl-apache.conf
|
||||||
|
|
||||||
# Uncomment the following to enable MediaProxy caching on disk
|
# Uncomment the following to enable MediaProxy caching on disk
|
||||||
#CacheRoot /tmp/akkoma-media-cache/
|
#CacheRoot /var/tmp/akkoma-media-cache/
|
||||||
#CacheDirLevels 1
|
#CacheDirLevels 1
|
||||||
#CacheDirLength 2
|
#CacheDirLength 2
|
||||||
#CacheEnable disk /proxy
|
#CacheEnable disk /proxy
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
SCRIPTNAME=${0##*/}
|
SCRIPTNAME=${0##*/}
|
||||||
|
|
||||||
# mod_disk_cache directory
|
# mod_disk_cache directory
|
||||||
CACHE_DIRECTORY="/tmp/akkoma-media-cache"
|
CACHE_DIRECTORY="/var/tmp/akkoma-media-cache"
|
||||||
|
|
||||||
## Removes an item via the htcacheclean utility
|
## Removes an item via the htcacheclean utility
|
||||||
## $1 - the filename, can be a pattern .
|
## $1 - the filename, can be a pattern .
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
# default nginx site config for Akkoma
|
# default nginx site config for Akkoma
|
||||||
#
|
#
|
||||||
# Simple installation instructions:
|
# See the documentation at docs.akkoma.dev for your particular distro/OS for
|
||||||
# 1. Install your TLS certificate, possibly using Let's Encrypt.
|
# installation instructions.
|
||||||
# 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.
|
|
||||||
|
|
||||||
proxy_cache_path /tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=10g
|
proxy_cache_path /var/tmp/akkoma-media-cache levels=1:2 keys_zone=akkoma_media_cache:10m max_size=1g
|
||||||
inactive=720m use_temp_path=off;
|
inactive=720m use_temp_path=off;
|
||||||
|
|
||||||
# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
|
# 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 127.0.0.1:4000 max_fails=5 fail_timeout=60s;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
# If you are setting up TLS certificates without certbot, uncomment the
|
||||||
server_name example.tld;
|
# following to enable HTTP -> HTTPS redirects. Certbot users don't need to do
|
||||||
|
# this as it will automatically do this for you.
|
||||||
listen 80;
|
# server {
|
||||||
listen [::]:80;
|
# server_name example.tld media.example.tld;
|
||||||
|
#
|
||||||
# Uncomment this if you need to use the 'webroot' method with certbot. Make sure
|
# listen 80;
|
||||||
# that the directory exists and that it is accessible by the webserver. If you followed
|
# listen [::]:80;
|
||||||
# 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
|
# location / {
|
||||||
# to get the certificate, and then uncomment it.
|
# return 301 https://$server_name$request_uri;
|
||||||
#
|
# }
|
||||||
# location ~ /\.well-known/acme-challenge {
|
# }
|
||||||
# root /var/lib/letsencrypt/;
|
|
||||||
# }
|
|
||||||
location / {
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Enable SSL session caching for improved performance
|
# Enable SSL session caching for improved performance
|
||||||
ssl_session_cache shared:ssl_session_cache:10m;
|
ssl_session_cache shared:ssl_session_cache:10m;
|
||||||
|
@ -41,22 +32,29 @@ ssl_session_cache shared:ssl_session_cache:10m;
|
||||||
server {
|
server {
|
||||||
server_name example.tld;
|
server_name example.tld;
|
||||||
|
|
||||||
listen 443 ssl http2;
|
# Once certbot is set up, this will automatically be updated to listen to
|
||||||
listen [::]:443 ssl http2;
|
# port 443 with TLS alongside a redirect from plaintext HTTP.
|
||||||
ssl_session_timeout 1d;
|
listen 80;
|
||||||
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
listen [::]:80;
|
||||||
ssl_session_tickets off;
|
|
||||||
|
|
||||||
ssl_trusted_certificate /etc/letsencrypt/live/example.tld/chain.pem;
|
# If you are not using Certbot, comment out the above and uncomment/edit the following
|
||||||
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
|
# listen 443 ssl http2;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
|
# 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_vary on;
|
||||||
gzip_proxied any;
|
gzip_proxied any;
|
||||||
|
@ -75,9 +73,43 @@ server {
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
location ~ ^/(media|proxy) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://phoenix;
|
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) {
|
location ~ ^/(media|proxy) {
|
||||||
proxy_cache akkoma_media_cache;
|
proxy_cache akkoma_media_cache;
|
||||||
|
@ -91,4 +123,8 @@ server {
|
||||||
chunked_transfer_encoding on;
|
chunked_transfer_encoding on;
|
||||||
proxy_pass http://phoenix;
|
proxy_pass http://phoenix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
SCRIPTNAME=${0##*/}
|
SCRIPTNAME=${0##*/}
|
||||||
|
|
||||||
# NGINX cache directory
|
# NGINX cache directory
|
||||||
CACHE_DIRECTORY="/tmp/akkoma-media-cache"
|
CACHE_DIRECTORY="/var/tmp/akkoma-media-cache"
|
||||||
|
|
||||||
## Return the files where the items are cached.
|
## Return the files where the items are cached.
|
||||||
## $1 - the filename, can be a pattern .
|
## $1 - the filename, can be a pattern .
|
||||||
|
|
|
@ -20,6 +20,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
||||||
output: :string,
|
output: :string,
|
||||||
output_psql: :string,
|
output_psql: :string,
|
||||||
domain: :string,
|
domain: :string,
|
||||||
|
media_url: :string,
|
||||||
instance_name: :string,
|
instance_name: :string,
|
||||||
admin_email: :string,
|
admin_email: :string,
|
||||||
notify_email: :string,
|
notify_email: :string,
|
||||||
|
@ -34,9 +35,9 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
||||||
static_dir: :string,
|
static_dir: :string,
|
||||||
listen_ip: :string,
|
listen_ip: :string,
|
||||||
listen_port: :string,
|
listen_port: :string,
|
||||||
strip_uploads: :string,
|
strip_uploads_metadata: :string,
|
||||||
anonymize_uploads: :string,
|
read_uploads_description: :string,
|
||||||
dedupe_uploads: :string
|
anonymize_uploads: :string
|
||||||
],
|
],
|
||||||
aliases: [
|
aliases: [
|
||||||
o: :output,
|
o: :output,
|
||||||
|
@ -64,6 +65,14 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
||||||
":"
|
":"
|
||||||
) ++ [443]
|
) ++ [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 =
|
name =
|
||||||
get_option(
|
get_option(
|
||||||
options,
|
options,
|
||||||
|
@ -161,21 +170,38 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
||||||
)
|
)
|
||||||
|> Path.expand()
|
|> Path.expand()
|
||||||
|
|
||||||
{strip_uploads_message, strip_uploads_default} =
|
{strip_uploads_metadata_message, strip_uploads_metadata_default} =
|
||||||
if Pleroma.Utils.command_available?("exiftool") do
|
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"}
|
"y"}
|
||||||
else
|
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"}
|
"n"}
|
||||||
end
|
end
|
||||||
|
|
||||||
strip_uploads =
|
strip_uploads_metadata =
|
||||||
get_option(
|
get_option(
|
||||||
options,
|
options,
|
||||||
:strip_uploads,
|
:strip_uploads_metadata,
|
||||||
strip_uploads_message,
|
strip_uploads_metadata_message,
|
||||||
strip_uploads_default
|
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"
|
) === "y"
|
||||||
|
|
||||||
anonymize_uploads =
|
anonymize_uploads =
|
||||||
|
@ -186,14 +212,6 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
||||||
"n"
|
"n"
|
||||||
) === "y"
|
) === "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)
|
Config.put([:instance, :static_dir], static_dir)
|
||||||
|
|
||||||
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
|
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(
|
EEx.eval_file(
|
||||||
template_dir <> "/sample_config.eex",
|
template_dir <> "/sample_config.eex",
|
||||||
domain: domain,
|
domain: domain,
|
||||||
|
media_url: media_url,
|
||||||
port: port,
|
port: port,
|
||||||
email: email,
|
email: email,
|
||||||
notify_email: notify_email,
|
notify_email: notify_email,
|
||||||
|
@ -229,9 +248,9 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
||||||
listen_port: listen_port,
|
listen_port: listen_port,
|
||||||
upload_filters:
|
upload_filters:
|
||||||
upload_filters(%{
|
upload_filters(%{
|
||||||
strip: strip_uploads,
|
strip_metadata: strip_uploads_metadata,
|
||||||
anonymize: anonymize_uploads,
|
read_description: read_uploads_description,
|
||||||
dedupe: dedupe_uploads
|
anonymize: anonymize_uploads
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -305,11 +324,20 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp upload_filters(filters) when is_map(filters) do
|
defp upload_filters(filters) when is_map(filters) do
|
||||||
|
enabled_filters = []
|
||||||
|
|
||||||
enabled_filters =
|
enabled_filters =
|
||||||
if filters.strip do
|
if filters.read_description do
|
||||||
[Pleroma.Upload.Filter.Exiftool]
|
enabled_filters ++ [Pleroma.Upload.Filter.Exiftool.ReadDescription]
|
||||||
else
|
else
|
||||||
[]
|
enabled_filters
|
||||||
|
end
|
||||||
|
|
||||||
|
enabled_filters =
|
||||||
|
if filters.strip_metadata do
|
||||||
|
enabled_filters ++ [Pleroma.Upload.Filter.Exiftool.StripMetadata]
|
||||||
|
else
|
||||||
|
enabled_filters
|
||||||
end
|
end
|
||||||
|
|
||||||
enabled_filters =
|
enabled_filters =
|
||||||
|
@ -319,13 +347,6 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
||||||
enabled_filters
|
enabled_filters
|
||||||
end
|
end
|
||||||
|
|
||||||
enabled_filters =
|
|
||||||
if filters.dedupe do
|
|
||||||
enabled_filters ++ [Pleroma.Upload.Filter.Dedupe]
|
|
||||||
else
|
|
||||||
enabled_filters
|
|
||||||
end
|
|
||||||
|
|
||||||
enabled_filters
|
enabled_filters
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
330
lib/mix/tasks/pleroma/security.ex
Normal file
330
lib/mix/tasks/pleroma/security.ex
Normal file
|
@ -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
|
|
@ -26,6 +26,15 @@ defmodule Pleroma.Activity.Pruner do
|
||||||
|> Repo.delete_all(timeout: :infinity)
|
|> Repo.delete_all(timeout: :infinity)
|
||||||
end
|
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
|
def prune_removes do
|
||||||
before_time = cutoff()
|
before_time = cutoff()
|
||||||
|
|
||||||
|
|
|
@ -179,7 +179,9 @@ defmodule Pleroma.Application do
|
||||||
build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500),
|
build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500),
|
||||||
build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500),
|
build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500),
|
||||||
build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000),
|
build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000),
|
||||||
build_cachex("rel_me", default_ttl: :timer.hours(24 * 30), limit: 300)
|
build_cachex("rel_me", default_ttl: :timer.hours(24 * 30), limit: 300),
|
||||||
|
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5000),
|
||||||
|
build_cachex("http_backoff", default_ttl: :timer.hours(24 * 30), limit: 10000)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -288,6 +290,7 @@ defmodule Pleroma.Application do
|
||||||
|> Config.get([])
|
|> Config.get([])
|
||||||
|> Pleroma.HTTP.AdapterHelper.add_pool_size(pool_size)
|
|> Pleroma.HTTP.AdapterHelper.add_pool_size(pool_size)
|
||||||
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy_pool(proxy)
|
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy_pool(proxy)
|
||||||
|
|> Pleroma.HTTP.AdapterHelper.ensure_ipv6()
|
||||||
|> Keyword.put(:name, MyFinch)
|
|> Keyword.put(:name, MyFinch)
|
||||||
|
|
||||||
[{Finch, config}]
|
[{Finch, config}]
|
||||||
|
|
|
@ -164,7 +164,8 @@ defmodule Pleroma.ApplicationRequirements do
|
||||||
|
|
||||||
defp check_system_commands!(:ok) do
|
defp check_system_commands!(:ok) do
|
||||||
filter_commands_statuses = [
|
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.Mogrify, "mogrify"),
|
||||||
check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"),
|
check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"),
|
||||||
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "mogrify"),
|
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "mogrify"),
|
||||||
|
|
|
@ -68,7 +68,10 @@ defmodule Akkoma.Collections.Fetcher do
|
||||||
items
|
items
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
{:error, {"Object has been deleted", _, _}} ->
|
{:error, :not_found} ->
|
||||||
|
items
|
||||||
|
|
||||||
|
{:error, :forbidden} ->
|
||||||
items
|
items
|
||||||
|
|
||||||
{:error, error} ->
|
{: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`"}
|
"\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
|
def check_simple_policy_tuples do
|
||||||
has_strings =
|
has_strings =
|
||||||
Config.get([:mrf_simple])
|
Config.get([:mrf_simple])
|
||||||
|
@ -182,7 +219,10 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
||||||
check_quarantined_instances_tuples(),
|
check_quarantined_instances_tuples(),
|
||||||
check_transparency_exclusions_tuples(),
|
check_transparency_exclusions_tuples(),
|
||||||
check_simple_policy_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
|
|> Enum.reduce(:ok, fn
|
||||||
:ok, :ok -> :ok
|
:ok, :ok -> :ok
|
||||||
|
@ -337,4 +377,54 @@ defmodule Pleroma.Config.DeprecationWarnings do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -64,4 +64,7 @@ defmodule Pleroma.Constants do
|
||||||
"Service"
|
"Service"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Internally used as top-level types for media attachments and user images
|
||||||
|
const(attachment_types, do: ["Document", "Image"])
|
||||||
end
|
end
|
||||||
|
|
|
@ -55,12 +55,61 @@ defmodule Pleroma.Emails.Mailer do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def validate_dependency do
|
def validate_dependency do
|
||||||
parse_config([])
|
parse_config([], defaults: false)
|
||||||
|> Keyword.get(:adapter)
|
|> Keyword.get(:adapter)
|
||||||
|> Swoosh.Mailer.validate_dependency()
|
|> Swoosh.Mailer.validate_dependency()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_config(config) do
|
defp ensure_charlist(input) do
|
||||||
Swoosh.Mailer.parse_config(@otp_app, __MODULE__, @mailer_config, config)
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,12 +26,37 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
alias Pleroma.Emoji.Pack
|
alias Pleroma.Emoji.Pack
|
||||||
alias Pleroma.Utils
|
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}
|
@spec create(String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
|
||||||
def create(name) do
|
def create(name) do
|
||||||
with :ok <- validate_not_empty([name]),
|
with :ok <- validate_not_empty([name]),
|
||||||
dir <- Path.join(emoji_path(), name),
|
dir <- path_join_name_safe(emoji_path(), name),
|
||||||
:ok <- File.mkdir(dir) do
|
: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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -65,7 +90,7 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
|
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
|
||||||
def delete(name) do
|
def delete(name) do
|
||||||
with :ok <- validate_not_empty([name]),
|
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)
|
File.rm_rf(pack_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -89,7 +114,7 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
end)
|
end)
|
||||||
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()}
|
{:ok, t()}
|
||||||
| {:error, File.posix() | atom()}
|
| {:error, File.posix() | atom()}
|
||||||
def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do
|
def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do
|
||||||
|
@ -107,7 +132,7 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
Enum.map_reduce(emojies, pack, fn item, emoji_pack ->
|
Enum.map_reduce(emojies, pack, fn item, emoji_pack ->
|
||||||
emoji_file = %Plug.Upload{
|
emoji_file = %Plug.Upload{
|
||||||
filename: item[:filename],
|
filename: item[:filename],
|
||||||
path: Path.join(tmp_dir, item[:path])
|
path: path_join_safe(tmp_dir, item[:path])
|
||||||
}
|
}
|
||||||
|
|
||||||
{:ok, updated_pack} =
|
{:ok, updated_pack} =
|
||||||
|
@ -137,6 +162,14 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_file(%Pack{} = pack, shortcode, filename, %Plug.Upload{} = file) do
|
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]),
|
with :ok <- validate_not_empty([shortcode, filename]),
|
||||||
:ok <- validate_emoji_not_exists(shortcode),
|
:ok <- validate_emoji_not_exists(shortcode),
|
||||||
{:ok, updated_pack} <- do_add_file(pack, shortcode, filename, file) do
|
{: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
|
{:ok, results} <- File.ls(emoji_path) do
|
||||||
names =
|
names =
|
||||||
results
|
results
|
||||||
|
# items come from File.ls, thus safe
|
||||||
|> Enum.map(&Path.join(emoji_path, &1))
|
|> Enum.map(&Path.join(emoji_path, &1))
|
||||||
|> Enum.reject(fn path ->
|
|> Enum.reject(fn path ->
|
||||||
File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
|
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()}
|
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()}
|
||||||
def load_pack(name) do
|
def load_pack(name) do
|
||||||
name = Path.basename(name)
|
pack_dir = path_join_name_safe(emoji_path(), name)
|
||||||
pack_file = Path.join([emoji_path(), name, "pack.json"])
|
pack_file = Path.join(pack_dir, "pack.json")
|
||||||
|
|
||||||
with {:ok, _} <- File.stat(pack_file),
|
with {:ok, _} <- File.stat(pack_file),
|
||||||
{:ok, pack_data} <- File.read(pack_file) do
|
{:ok, pack_data} <- File.read(pack_file) do
|
||||||
|
@ -412,7 +446,13 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp create_archive_and_cache(pack, hash) do
|
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}} =
|
{:ok, {_, result}} =
|
||||||
:zip.zip(~c"#{pack.name}.zip", files, [:memory, cwd: to_charlist(pack.path)])
|
:zip.zip(~c"#{pack.name}.zip", files, [:memory, cwd: to_charlist(pack.path)])
|
||||||
|
@ -474,7 +514,7 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp save_file(%Plug.Upload{path: upload_path}, pack, filename) do
|
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)
|
create_subdirs(file_path)
|
||||||
|
|
||||||
with {:ok, _} <- File.copy(upload_path, file_path) do
|
with {:ok, _} <- File.copy(upload_path, file_path) do
|
||||||
|
@ -482,6 +522,12 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
end
|
end
|
||||||
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
|
defp put_emoji(pack, shortcode, filename) do
|
||||||
files = Map.put(pack.files, shortcode, filename)
|
files = Map.put(pack.files, shortcode, filename)
|
||||||
%{pack | files: files, files_count: length(Map.keys(files))}
|
%{pack | files: files, files_count: length(Map.keys(files))}
|
||||||
|
@ -493,8 +539,8 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp rename_file(pack, filename, new_filename) do
|
defp rename_file(pack, filename, new_filename) do
|
||||||
old_path = Path.join(pack.path, filename)
|
old_path = path_join_safe(pack.path, filename)
|
||||||
new_path = Path.join(pack.path, new_filename)
|
new_path = path_join_safe(pack.path, new_filename)
|
||||||
create_subdirs(new_path)
|
create_subdirs(new_path)
|
||||||
|
|
||||||
with :ok <- File.rename(old_path, new_path) do
|
with :ok <- File.rename(old_path, new_path) do
|
||||||
|
@ -512,7 +558,7 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
|
|
||||||
defp remove_file(pack, shortcode) do
|
defp remove_file(pack, shortcode) do
|
||||||
with {:ok, filename} <- get_filename(pack, shortcode),
|
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
|
:ok <- File.rm(emoji) do
|
||||||
remove_dir_if_empty(emoji, filename)
|
remove_dir_if_empty(emoji, filename)
|
||||||
end
|
end
|
||||||
|
@ -530,7 +576,7 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
|
|
||||||
defp get_filename(pack, shortcode) do
|
defp get_filename(pack, shortcode) do
|
||||||
with %{^shortcode => filename} when is_binary(filename) <- pack.files,
|
with %{^shortcode => filename} when is_binary(filename) <- pack.files,
|
||||||
file_path <- Path.join(pack.path, filename),
|
file_path <- path_join_safe(pack.path, filename),
|
||||||
{:ok, _} <- File.stat(file_path) do
|
{:ok, _} <- File.stat(file_path) do
|
||||||
{:ok, filename}
|
{:ok, filename}
|
||||||
else
|
else
|
||||||
|
@ -568,7 +614,7 @@ defmodule Pleroma.Emoji.Pack do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp copy_as(remote_pack, local_name) do
|
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__{
|
%__MODULE__{
|
||||||
name: local_name,
|
name: local_name,
|
||||||
|
|
|
@ -65,6 +65,15 @@ defmodule Pleroma.HTTP.AdapterHelper do
|
||||||
|> put_in([:pools, :default, :size], pool_size)
|
|> put_in([:pools, :default, :size], pool_size)
|
||||||
end
|
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
|
defp maybe_add_pools(opts) do
|
||||||
if Keyword.has_key?(opts, :pools) do
|
if Keyword.has_key?(opts, :pools) do
|
||||||
opts
|
opts
|
||||||
|
@ -96,11 +105,15 @@ defmodule Pleroma.HTTP.AdapterHelper do
|
||||||
defp maybe_add_transport_opts(opts) do
|
defp maybe_add_transport_opts(opts) do
|
||||||
transport_opts = get_in(opts, [:pools, :default, :conn_opts, :transport_opts])
|
transport_opts = get_in(opts, [:pools, :default, :conn_opts, :transport_opts])
|
||||||
|
|
||||||
unless is_nil(transport_opts) do
|
opts =
|
||||||
opts
|
unless is_nil(transport_opts) do
|
||||||
else
|
opts
|
||||||
put_in(opts, [:pools, :default, :conn_opts, :transport_opts], [])
|
else
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
121
lib/pleroma/http/backoff.ex
Normal file
121
lib/pleroma/http/backoff.ex
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
defmodule Pleroma.HTTP.Backoff do
|
||||||
|
alias Pleroma.HTTP
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||||
|
@backoff_cache :http_backoff_cache
|
||||||
|
|
||||||
|
# attempt to parse a timestamp from a header
|
||||||
|
# returns nil if it can't parse the timestamp
|
||||||
|
@spec timestamp_or_nil(binary) :: DateTime.t() | nil
|
||||||
|
defp timestamp_or_nil(header) do
|
||||||
|
case DateTime.from_iso8601(header) do
|
||||||
|
{:ok, stamp, _} ->
|
||||||
|
stamp
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# attempt to parse the x-ratelimit-reset header from the headers
|
||||||
|
@spec x_ratelimit_reset(headers :: list) :: DateTime.t() | nil
|
||||||
|
defp x_ratelimit_reset(headers) do
|
||||||
|
with {_header, value} <- List.keyfind(headers, "x-ratelimit-reset", 0),
|
||||||
|
true <- is_binary(value) do
|
||||||
|
timestamp_or_nil(value)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# attempt to parse the Retry-After header from the headers
|
||||||
|
# this can be either a timestamp _or_ a number of seconds to wait!
|
||||||
|
# we'll return a datetime if we can parse it, or nil if we can't
|
||||||
|
@spec retry_after(headers :: list) :: DateTime.t() | nil
|
||||||
|
defp retry_after(headers) do
|
||||||
|
with {_header, value} <- List.keyfind(headers, "retry-after", 0),
|
||||||
|
true <- is_binary(value) do
|
||||||
|
# first, see if it's an integer
|
||||||
|
case Integer.parse(value) do
|
||||||
|
{seconds, ""} ->
|
||||||
|
Logger.debug("Parsed Retry-After header: #{seconds} seconds")
|
||||||
|
DateTime.utc_now() |> Timex.shift(seconds: seconds)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# if it's not an integer, try to parse it as a timestamp
|
||||||
|
timestamp_or_nil(value)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# given a set of headers, will attempt to find the next backoff timestamp
|
||||||
|
# if it can't find one, it will default to 5 minutes from now
|
||||||
|
@spec next_backoff_timestamp(%{headers: list}) :: DateTime.t()
|
||||||
|
defp next_backoff_timestamp(%{headers: headers}) when is_list(headers) do
|
||||||
|
default_5_minute_backoff =
|
||||||
|
DateTime.utc_now()
|
||||||
|
|> Timex.shift(seconds: 5 * 60)
|
||||||
|
|
||||||
|
backoff =
|
||||||
|
[&x_ratelimit_reset/1, &retry_after/1]
|
||||||
|
|> Enum.map(& &1.(headers))
|
||||||
|
|> Enum.find(&(&1 != nil))
|
||||||
|
|
||||||
|
if is_nil(backoff) do
|
||||||
|
Logger.debug("No backoff headers found, defaulting to 5 minutes from now")
|
||||||
|
default_5_minute_backoff
|
||||||
|
else
|
||||||
|
Logger.debug("Found backoff header, will back off until: #{backoff}")
|
||||||
|
backoff
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp next_backoff_timestamp(_), do: DateTime.utc_now() |> Timex.shift(seconds: 5 * 60)
|
||||||
|
|
||||||
|
# utility function to check the HTTP response for potential backoff headers
|
||||||
|
# will check if we get a 429 or 503 response, and if we do, will back off for a bit
|
||||||
|
@spec check_backoff({:ok | :error, HTTP.Env.t()}, binary()) ::
|
||||||
|
{:ok | :error, HTTP.Env.t()} | {:error, :ratelimit}
|
||||||
|
defp check_backoff({:ok, env}, host) do
|
||||||
|
case env.status do
|
||||||
|
status when status in [429, 503] ->
|
||||||
|
Logger.error("Rate limited on #{host}! Backing off...")
|
||||||
|
timestamp = next_backoff_timestamp(env)
|
||||||
|
ttl = Timex.diff(timestamp, DateTime.utc_now(), :seconds)
|
||||||
|
# we will cache the host for 5 minutes
|
||||||
|
@cachex.put(@backoff_cache, host, true, ttl: ttl)
|
||||||
|
{:error, :ratelimit}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:ok, env}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp check_backoff(env, _), do: env
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
this acts as a single throughput for all GET requests
|
||||||
|
we will check if the host is in the cache, and if it is, we will automatically fail the request
|
||||||
|
this ensures that we don't hammer the server with requests, and instead wait for the backoff to expire
|
||||||
|
this is a very simple implementation, and can be improved upon!
|
||||||
|
"""
|
||||||
|
@spec get(binary, list, list) :: {:ok | :error, HTTP.Env.t()} | {:error, :ratelimit}
|
||||||
|
def get(url, headers \\ [], options \\ []) do
|
||||||
|
%{host: host} = URI.parse(url)
|
||||||
|
|
||||||
|
case @cachex.get(@backoff_cache, host) do
|
||||||
|
{:ok, nil} ->
|
||||||
|
url
|
||||||
|
|> HTTP.get(headers, options)
|
||||||
|
|> check_backoff(host)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :ratelimit}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -178,7 +178,10 @@ defmodule Pleroma.Object do
|
||||||
ap_id
|
ap_id
|
||||||
|
|
||||||
Keyword.get(options, :fetch) ->
|
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 ->
|
true ->
|
||||||
get_cached_by_ap_id(ap_id)
|
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
|
Object containment is an important step in validating remote objects to prevent
|
||||||
spoofing, therefore removal of object containment functions is NOT recommended.
|
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
|
def get_actor(%{"actor" => actor}) when is_binary(actor) do
|
||||||
actor
|
actor
|
||||||
end
|
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(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok
|
||||||
defp compare_uris(_id_uri, _other_uri), do: :error
|
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 """
|
@doc """
|
||||||
Checks that an imported AP object's actor matches the host it came from.
|
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),
|
def contain_origin(id, %{"attributedTo" => actor} = params),
|
||||||
do: contain_origin(id, Map.put(params, "actor", actor))
|
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
|
def contain_origin_from_id(id, %{"id" => other_id} = _params) when is_binary(other_id) do
|
||||||
id_uri = URI.parse(id)
|
id_uri = URI.parse(id)
|
||||||
other_uri = URI.parse(other_id)
|
other_uri = URI.parse(other_id)
|
||||||
|
@ -85,4 +136,12 @@ defmodule Pleroma.Object.Containment do
|
||||||
do: contain_origin(id, object)
|
do: contain_origin(id, object)
|
||||||
|
|
||||||
def contain_child(_), do: :ok
|
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
|
end
|
||||||
|
|
|
@ -18,6 +18,16 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
require Logger
|
require Logger
|
||||||
require Pleroma.Constants
|
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
|
defp touch_changeset(changeset) do
|
||||||
updated_at =
|
updated_at =
|
||||||
NaiveDateTime.utc_now()
|
NaiveDateTime.utc_now()
|
||||||
|
@ -103,54 +113,78 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
end
|
end
|
||||||
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
|
def refetch_object(%Object{data: %{"id" => id}} = object) do
|
||||||
with {:local, false} <- {:local, Object.local?(object)},
|
with {:local, false} <- {:local, Object.local?(object)},
|
||||||
{:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
|
{: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} <- reinject_object(object, new_data) do
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
else
|
else
|
||||||
{:local, true} -> {:ok, object}
|
{:local, true} -> {:ok, object}
|
||||||
|
{:id, false} -> {:error, :id_mismatch}
|
||||||
e -> {:error, e}
|
e -> {:error, e}
|
||||||
end
|
end
|
||||||
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
|
def fetch_object_from_id(id, options \\ []) do
|
||||||
with %URI{} = uri <- URI.parse(id),
|
with %URI{} = uri <- URI.parse(id),
|
||||||
# let's check the URI is even vaguely valid first
|
# 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
|
# If we have instance restrictions, apply them here to prevent fetching from unwanted instances
|
||||||
{:ok, nil} <- Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_reject(uri),
|
{:mrf_reject_check, {:ok, nil}} <-
|
||||||
{:ok, _} <- Pleroma.Web.ActivityPub.MRF.SimplePolicy.check_accept(uri),
|
{: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)},
|
{_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
|
||||||
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
|
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
|
||||||
{_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
|
{_, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
|
||||||
{_, nil} <- {:normalize, Object.normalize(data, fetch: false)},
|
{_, nil} <- {:normalize, Object.normalize(data, fetch: false)},
|
||||||
params <- prepare_activity_params(data),
|
params <- prepare_activity_params(data),
|
||||||
{_, :ok} <- {:containment, Containment.contain_origin(id, params)},
|
|
||||||
{_, {:ok, activity}} <-
|
{_, {:ok, activity}} <-
|
||||||
{:transmogrifier, Transmogrifier.handle_incoming(params, options)},
|
{:transmogrifier, Transmogrifier.handle_incoming(params, options)},
|
||||||
{_, _data, %Object{} = object} <-
|
{_, _data, %Object{} = object} <-
|
||||||
{:object, data, Object.normalize(activity, fetch: false)} do
|
{:object, data, Object.normalize(activity, fetch: false)} do
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
else
|
else
|
||||||
{:allowed_depth, false} ->
|
{:allowed_depth, false} = e ->
|
||||||
{:error, "Max thread distance exceeded."}
|
log_fetch_error(id, e)
|
||||||
|
{:error, :allowed_depth}
|
||||||
|
|
||||||
{:scheme, false} ->
|
{:valid_uri_scheme, _} = e ->
|
||||||
{:error, "URI Scheme Invalid"}
|
log_fetch_error(id, e)
|
||||||
|
{:error, :invalid_uri_scheme}
|
||||||
|
|
||||||
{:containment, _} ->
|
{:mrf_reject_check, _} = e ->
|
||||||
{:error, "Object containment failed."}
|
log_fetch_error(id, e)
|
||||||
|
{:reject, :mrf}
|
||||||
|
|
||||||
{:transmogrifier, {:error, {:reject, e}}} ->
|
{:mrf_accept_check, _} = e ->
|
||||||
{:reject, e}
|
log_fetch_error(id, e)
|
||||||
|
{:reject, :mrf}
|
||||||
|
|
||||||
{:transmogrifier, {:reject, e}} ->
|
{:containment, reason} = e ->
|
||||||
{:reject, e}
|
log_fetch_error(id, e)
|
||||||
|
{:error, reason}
|
||||||
|
|
||||||
{:transmogrifier, _} = e ->
|
{:transmogrifier, {:error, {:reject, reason}}} = e ->
|
||||||
{:error, 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} ->
|
{:object, data, nil} ->
|
||||||
reinject_object(%Object{}, data)
|
reinject_object(%Object{}, data)
|
||||||
|
@ -161,17 +195,21 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
{:fetch_object, %Object{} = object} ->
|
{:fetch_object, %Object{} = object} ->
|
||||||
{:ok, object}
|
{:ok, object}
|
||||||
|
|
||||||
{:fetch, {:error, error}} ->
|
{:fetch, {:error, reason}} = e ->
|
||||||
{:error, error}
|
log_fetch_error(id, e)
|
||||||
|
{:error, reason}
|
||||||
{:reject, reason} ->
|
|
||||||
{:reject, reason}
|
|
||||||
|
|
||||||
e ->
|
e ->
|
||||||
e
|
log_fetch_error(id, e)
|
||||||
|
{:error, e}
|
||||||
end
|
end
|
||||||
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
|
defp prepare_activity_params(data) do
|
||||||
%{
|
%{
|
||||||
"type" => "Create",
|
"type" => "Create",
|
||||||
|
@ -185,26 +223,6 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
|> Maps.put_if_present("bcc", data["bcc"])
|
|> Maps.put_if_present("bcc", data["bcc"])
|
||||||
end
|
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
|
defp make_signature(id, date) do
|
||||||
uri = URI.parse(id)
|
uri = URI.parse(id)
|
||||||
|
|
||||||
|
@ -235,6 +253,7 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
end
|
end
|
||||||
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)
|
||||||
|
|
||||||
def fetch_and_contain_remote_object_from_id(%{"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
|
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
||||||
Logger.debug("Fetching object #{id} via AP")
|
Logger.debug("Fetching object #{id} via AP")
|
||||||
|
|
||||||
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
|
with {:valid_uri_scheme, true} <- {:valid_uri_scheme, String.starts_with?(id, "http")},
|
||||||
{:ok, body} <- get_object(id),
|
%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, data} <- safe_json_decode(body),
|
||||||
:ok <- Containment.contain_origin_from_id(id, data) do
|
{_, :ok} <- {:strict_id, Containment.contain_id_to_fetch(final_id, data)},
|
||||||
unless Instances.reachable?(id) do
|
{_, :ok} <- {:containment, Containment.contain_origin(final_id, data)} do
|
||||||
Instances.set_reachable(id)
|
unless Instances.reachable?(final_id) do
|
||||||
|
Instances.set_reachable(final_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok, data}
|
{:ok, data}
|
||||||
else
|
else
|
||||||
{:scheme, _} ->
|
{:strict_id, _} = e ->
|
||||||
{:error, "Unsupported URI scheme"}
|
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} ->
|
||||||
{:error, e}
|
{:error, e}
|
||||||
|
@ -265,47 +312,85 @@ defmodule Pleroma.Object.Fetcher do
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_and_contain_remote_object_from_id(_id),
|
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
|
def get_object(id) do
|
||||||
date = Pleroma.Signature.signed_date()
|
date = Pleroma.Signature.signed_date()
|
||||||
|
|
||||||
headers =
|
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)
|
|> maybe_date_fetch(date)
|
||||||
|> sign_fetch(id, date)
|
|> sign_fetch(id, date)
|
||||||
|
|
||||||
case HTTP.get(id, headers) do
|
with {:ok, %{body: body, status: code, headers: headers, url: final_url}}
|
||||||
{:ok, %{body: body, status: code, headers: headers}} when code in 200..299 ->
|
when code in 200..299 <-
|
||||||
case List.keyfind(headers, "content-type", 0) do
|
HTTP.Backoff.get(id, headers),
|
||||||
{_, content_type} ->
|
remote_host <-
|
||||||
case Plug.Conn.Utils.media_type(content_type) do
|
URI.parse(final_url).host,
|
||||||
{:ok, "application", "activity+json", _} ->
|
{:cross_domain_redirect, false} <-
|
||||||
{:ok, body}
|
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",
|
case {subtype, type_params} do
|
||||||
%{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
|
{"activity+json", _} ->
|
||||||
{:ok, body}
|
{:ok, final_id, body}
|
||||||
|
|
||||||
# pixelfed sometimes (and only sometimes) responds with http instead of https
|
{"ld+json", %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
|
||||||
{:ok, "application", "ld+json",
|
{:ok, final_id, body}
|
||||||
%{"profile" => "http://www.w3.org/ns/activitystreams"}} ->
|
|
||||||
{:ok, body}
|
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
{:error, {:content_type, content_type}}
|
{:error, {:content_type, content_type}}
|
||||||
end
|
end
|
||||||
|
else
|
||||||
_ ->
|
{:ok, %{status: code}} when code in [401, 403] ->
|
||||||
{:error, {:content_type, nil}}
|
{:error, :forbidden}
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, %{status: code}} when code in [404, 410] ->
|
{:ok, %{status: code}} when code in [404, 410] ->
|
||||||
{:error, {"Object has been deleted", id, code}}
|
{:error, :not_found}
|
||||||
|
|
||||||
{:error, e} ->
|
{:error, e} ->
|
||||||
{:error, e}
|
{:error, e}
|
||||||
|
|
||||||
|
{:has_content_type, _} ->
|
||||||
|
{:error, {:content_type, nil}}
|
||||||
|
|
||||||
|
{:parse_content_type, e} ->
|
||||||
|
{:error, {:content_type, e}}
|
||||||
|
|
||||||
e ->
|
e ->
|
||||||
{:error, e}
|
{:error, e}
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,6 +17,8 @@ defmodule Pleroma.ReverseProxy do
|
||||||
@failed_request_ttl :timer.seconds(60)
|
@failed_request_ttl :timer.seconds(60)
|
||||||
@methods ~w(GET HEAD)
|
@methods ~w(GET HEAD)
|
||||||
|
|
||||||
|
@allowed_mime_types Pleroma.Config.get([Pleroma.Upload, :allowed_mime_types], [])
|
||||||
|
|
||||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||||
|
|
||||||
def max_read_duration_default, do: @max_read_duration
|
def max_read_duration_default, do: @max_read_duration
|
||||||
|
@ -253,6 +255,7 @@ defmodule Pleroma.ReverseProxy do
|
||||||
headers
|
headers
|
||||||
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|
||||||
|> build_resp_cache_headers(opts)
|
|> build_resp_cache_headers(opts)
|
||||||
|
|> sanitise_content_type()
|
||||||
|> build_resp_content_disposition_header(opts)
|
|> build_resp_content_disposition_header(opts)
|
||||||
|> build_csp_headers()
|
|> build_csp_headers()
|
||||||
|> Keyword.merge(Keyword.get(opts, :resp_headers, []))
|
|> Keyword.merge(Keyword.get(opts, :resp_headers, []))
|
||||||
|
@ -282,6 +285,21 @@ defmodule Pleroma.ReverseProxy do
|
||||||
end
|
end
|
||||||
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
|
defp build_resp_content_disposition_header(headers, opts) do
|
||||||
opt = Keyword.get(opts, :inline_content_types, @inline_content_types)
|
opt = Keyword.get(opts, :inline_content_types, @inline_content_types)
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
def changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
|
defp changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
|
||||||
scheduled_activity
|
scheduled_activity
|
||||||
|> cast(attrs, [:scheduled_at, :params])
|
|> cast(attrs, [:scheduled_at, :params])
|
||||||
|> validate_required([:scheduled_at, :params])
|
|> validate_required([:scheduled_at, :params])
|
||||||
|
@ -40,26 +40,36 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
|
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
|
||||||
)
|
)
|
||||||
when is_list(media_ids) do
|
when is_list(media_ids) do
|
||||||
media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids})
|
user = User.get_by_id(changeset.data.user_id)
|
||||||
|
|
||||||
params =
|
case Utils.attachments_from_ids(user, %{media_ids: media_ids}) do
|
||||||
params
|
media_attachments when is_list(media_attachments) ->
|
||||||
|> Map.put("media_attachments", media_attachments)
|
params =
|
||||||
|> Map.put("media_ids", media_ids)
|
params
|
||||||
|
|> Map.put("media_attachments", media_attachments)
|
||||||
|
|> Map.put("media_ids", media_ids)
|
||||||
|
|
||||||
put_change(changeset, :params, params)
|
put_change(changeset, :params, params)
|
||||||
|
|
||||||
|
{:error, _} = e ->
|
||||||
|
e
|
||||||
|
|
||||||
|
e ->
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp with_media_attachments(changeset), do: changeset
|
defp with_media_attachments(changeset), do: changeset
|
||||||
|
|
||||||
def update_changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
|
defp update_changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
|
||||||
|
# note: should this ever allow swapping media attachments, make sure ownership is checked
|
||||||
scheduled_activity
|
scheduled_activity
|
||||||
|> cast(attrs, [:scheduled_at])
|
|> cast(attrs, [:scheduled_at])
|
||||||
|> validate_required([:scheduled_at])
|
|> validate_required([:scheduled_at])
|
||||||
|> validate_scheduled_at()
|
|> validate_scheduled_at()
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_scheduled_at(changeset) do
|
defp validate_scheduled_at(changeset) do
|
||||||
validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
|
validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
|
||||||
cond do
|
cond do
|
||||||
not far_enough?(scheduled_at) ->
|
not far_enough?(scheduled_at) ->
|
||||||
|
@ -77,7 +87,7 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def exceeds_daily_user_limit?(user_id, scheduled_at) do
|
defp exceeds_daily_user_limit?(user_id, scheduled_at) do
|
||||||
ScheduledActivity
|
ScheduledActivity
|
||||||
|> where(user_id: ^user_id)
|
|> where(user_id: ^user_id)
|
||||||
|> where([sa], type(sa.scheduled_at, :date) == type(^scheduled_at, :date))
|
|> where([sa], type(sa.scheduled_at, :date) == type(^scheduled_at, :date))
|
||||||
|
@ -86,7 +96,7 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
|> Kernel.>=(Config.get([ScheduledActivity, :daily_user_limit]))
|
|> Kernel.>=(Config.get([ScheduledActivity, :daily_user_limit]))
|
||||||
end
|
end
|
||||||
|
|
||||||
def exceeds_total_user_limit?(user_id) do
|
defp exceeds_total_user_limit?(user_id) do
|
||||||
ScheduledActivity
|
ScheduledActivity
|
||||||
|> where(user_id: ^user_id)
|
|> where(user_id: ^user_id)
|
||||||
|> select([sa], count(sa.id))
|
|> select([sa], count(sa.id))
|
||||||
|
@ -108,20 +118,29 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
diff > @min_offset
|
diff > @min_offset
|
||||||
end
|
end
|
||||||
|
|
||||||
def new(%User{} = user, attrs) do
|
defp new(%User{} = user, attrs) do
|
||||||
changeset(%ScheduledActivity{user_id: user.id}, attrs)
|
changeset(%ScheduledActivity{user_id: user.id}, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates ScheduledActivity and add to queue to perform at scheduled_at date
|
Creates ScheduledActivity and add to queue to perform at scheduled_at date
|
||||||
"""
|
"""
|
||||||
@spec create(User.t(), map()) :: {:ok, ScheduledActivity.t()} | {:error, Ecto.Changeset.t()}
|
@spec create(User.t(), map()) :: {:ok, ScheduledActivity.t()} | {:error, any()}
|
||||||
def create(%User{} = user, attrs) do
|
def create(%User{} = user, attrs) do
|
||||||
Multi.new()
|
case new(user, attrs) do
|
||||||
|> Multi.insert(:scheduled_activity, new(user, attrs))
|
%Ecto.Changeset{} = sched_data ->
|
||||||
|> maybe_add_jobs(Config.get([ScheduledActivity, :enabled]))
|
Multi.new()
|
||||||
|> Repo.transaction()
|
|> Multi.insert(:scheduled_activity, sched_data)
|
||||||
|> transaction_response
|
|> maybe_add_jobs(Config.get([ScheduledActivity, :enabled]))
|
||||||
|
|> Repo.transaction()
|
||||||
|
|> transaction_response
|
||||||
|
|
||||||
|
{:error, _} = e ->
|
||||||
|
e
|
||||||
|
|
||||||
|
e ->
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_add_jobs(multi, true) do
|
defp maybe_add_jobs(multi, true) do
|
||||||
|
@ -187,17 +206,7 @@ defmodule Pleroma.ScheduledActivity do
|
||||||
|> where(user_id: ^user.id)
|
|> where(user_id: ^user.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def due_activities(offset \\ 0) do
|
defp job_query(scheduled_activity_id) do
|
||||||
naive_datetime =
|
|
||||||
NaiveDateTime.utc_now()
|
|
||||||
|> NaiveDateTime.add(offset, :millisecond)
|
|
||||||
|
|
||||||
ScheduledActivity
|
|
||||||
|> where([sa], sa.scheduled_at < ^naive_datetime)
|
|
||||||
|> Repo.all()
|
|
||||||
end
|
|
||||||
|
|
||||||
def job_query(scheduled_activity_id) do
|
|
||||||
from(j in Oban.Job,
|
from(j in Oban.Job,
|
||||||
where: j.queue == "scheduled_activities",
|
where: j.queue == "scheduled_activities",
|
||||||
where: fragment("args ->> 'activity_id' = ?::text", ^to_string(scheduled_activity_id))
|
where: fragment("args ->> 'activity_id' = ?::text", ^to_string(scheduled_activity_id))
|
||||||
|
|
|
@ -10,7 +10,7 @@ defmodule Pleroma.Signature do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
|
||||||
@known_suffixes ["/publickey", "/main-key"]
|
@known_suffixes ["/publickey", "/main-key", "#key"]
|
||||||
|
|
||||||
def key_id_to_actor_id(key_id) do
|
def key_id_to_actor_id(key_id) do
|
||||||
uri =
|
uri =
|
||||||
|
|
|
@ -13,7 +13,6 @@ defmodule Pleroma.Upload do
|
||||||
* `:uploader`: override uploader
|
* `:uploader`: override uploader
|
||||||
* `:filters`: override filters
|
* `:filters`: override filters
|
||||||
* `:size_limit`: override size limit
|
* `:size_limit`: override size limit
|
||||||
* `:activity_type`: override activity type
|
|
||||||
|
|
||||||
The `%Pleroma.Upload{}` struct: all documented fields are meant to be overwritten in filters:
|
The `%Pleroma.Upload{}` struct: all documented fields are meant to be overwritten in filters:
|
||||||
|
|
||||||
|
@ -48,7 +47,6 @@ defmodule Pleroma.Upload do
|
||||||
@type option ::
|
@type option ::
|
||||||
{:type, :avatar | :banner | :background}
|
{:type, :avatar | :banner | :background}
|
||||||
| {:description, String.t()}
|
| {:description, String.t()}
|
||||||
| {:activity_type, String.t()}
|
|
||||||
| {:size_limit, nil | non_neg_integer()}
|
| {:size_limit, nil | non_neg_integer()}
|
||||||
| {:uploader, module()}
|
| {:uploader, module()}
|
||||||
| {:filters, [module()]}
|
| {:filters, [module()]}
|
||||||
|
@ -61,12 +59,23 @@ defmodule Pleroma.Upload do
|
||||||
width: integer(),
|
width: integer(),
|
||||||
height: integer(),
|
height: integer(),
|
||||||
blurhash: String.t(),
|
blurhash: String.t(),
|
||||||
|
description: String.t(),
|
||||||
path: 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()}
|
@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."
|
@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 +85,7 @@ defmodule Pleroma.Upload do
|
||||||
with {:ok, upload} <- prepare_upload(upload, opts),
|
with {:ok, upload} <- prepare_upload(upload, opts),
|
||||||
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
|
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
|
||||||
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
|
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
|
||||||
description = Map.get(opts, :description) || "",
|
description = Map.get(upload, :description) || "",
|
||||||
{_, true} <-
|
{_, true} <-
|
||||||
{:description_limit,
|
{:description_limit,
|
||||||
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
|
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
|
||||||
|
@ -132,7 +141,7 @@ defmodule Pleroma.Upload do
|
||||||
end
|
end
|
||||||
|
|
||||||
%{
|
%{
|
||||||
activity_type: Keyword.get(opts, :activity_type, activity_type),
|
activity_type: activity_type,
|
||||||
size_limit: Keyword.get(opts, :size_limit, size_limit),
|
size_limit: Keyword.get(opts, :size_limit, size_limit),
|
||||||
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
|
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
|
||||||
filters:
|
filters:
|
||||||
|
@ -152,7 +161,8 @@ defmodule Pleroma.Upload do
|
||||||
id: UUID.generate(),
|
id: UUID.generate(),
|
||||||
name: file.filename,
|
name: file.filename,
|
||||||
tempfile: file.path,
|
tempfile: file.path,
|
||||||
content_type: file.content_type
|
content_type: file.content_type,
|
||||||
|
description: opts.description
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -172,7 +182,8 @@ defmodule Pleroma.Upload do
|
||||||
id: UUID.generate(),
|
id: UUID.generate(),
|
||||||
name: hash <> "." <> ext,
|
name: hash <> "." <> ext,
|
||||||
tempfile: tmp_path,
|
tempfile: tmp_path,
|
||||||
content_type: content_type
|
content_type: content_type,
|
||||||
|
description: opts.description
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -235,7 +246,7 @@ defmodule Pleroma.Upload do
|
||||||
|
|
||||||
case uploader do
|
case uploader do
|
||||||
Pleroma.Uploaders.Local ->
|
Pleroma.Uploaders.Local ->
|
||||||
upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/"
|
upload_base_url
|
||||||
|
|
||||||
Pleroma.Uploaders.S3 ->
|
Pleroma.Uploaders.S3 ->
|
||||||
bucket = Config.get([Pleroma.Uploaders.S3, :bucket])
|
bucket = Config.get([Pleroma.Uploaders.S3, :bucket])
|
||||||
|
@ -261,7 +272,7 @@ defmodule Pleroma.Upload do
|
||||||
end
|
end
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
public_endpoint || upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/"
|
public_endpoint || upload_base_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
51
lib/pleroma/upload/filter/exiftool/read_description.ex
Normal file
51
lib/pleroma/upload/filter/exiftool/read_description.ex
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# 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", "-ignoreMinorErrors", "-q", "-q", tag, file],
|
||||||
|
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/>
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Upload.Filter.Exiftool do
|
defmodule Pleroma.Upload.Filter.Exiftool.StripMetadata do
|
||||||
@moduledoc """
|
@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.
|
Also strips or replaces filesystem metadata e.g., timestamps.
|
||||||
"""
|
"""
|
||||||
@behaviour Pleroma.Upload.Filter
|
@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()}
|
@spec filter(Pleroma.Upload.t()) :: {:ok, :noop} | {:ok, :filtered} | {:error, String.t()}
|
||||||
|
|
||||||
# Formats not compatible with exiftool at this time
|
# 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/webp"}), do: {:ok, :noop}
|
||||||
def filter(%Pleroma.Upload{content_type: "image/svg+xml"}), 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
|
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
|
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}
|
{_response, 0} -> {:ok, :filtered}
|
||||||
{error, 1} -> {:error, error}
|
{error, 1} -> {:error, error}
|
||||||
end
|
end
|
|
@ -969,15 +969,16 @@ defmodule Pleroma.User do
|
||||||
|
|
||||||
defp maybe_send_registration_email(_), do: {:ok, :noop}
|
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, options) do
|
||||||
|
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >=
|
||||||
def needs_update?(%User{local: false} = user) do
|
Keyword.get(options, :maximum_age, 86_400)
|
||||||
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def needs_update?(_), do: true
|
def needs_update?(_, _options), do: true
|
||||||
|
|
||||||
# "Locked" (self-locked) users demand explicit authorization of follow requests
|
# "Locked" (self-locked) users demand explicit authorization of follow requests
|
||||||
@spec can_direct_follow_local(User.t(), User.t()) :: true | false
|
@spec can_direct_follow_local(User.t(), User.t()) :: true | false
|
||||||
|
@ -1980,10 +1981,10 @@ defmodule Pleroma.User do
|
||||||
|
|
||||||
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
|
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)
|
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
|
case {cached_user, maybe_fetched_user} do
|
||||||
{_, {:ok, %User{} = user}} ->
|
{_, {:ok, %User{} = user}} ->
|
||||||
|
|
|
@ -22,6 +22,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
alias Pleroma.Upload
|
alias Pleroma.Upload
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.MRF
|
alias Pleroma.Web.ActivityPub.MRF
|
||||||
|
alias Pleroma.Web.ActivityPub.ObjectValidators.UserValidator
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
alias Pleroma.Web.Streamer
|
alias Pleroma.Web.Streamer
|
||||||
alias Pleroma.Web.WebFinger
|
alias Pleroma.Web.WebFinger
|
||||||
|
@ -1544,11 +1545,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
defp normalize_also_known_as(aka) when is_binary(aka), do: [aka]
|
defp normalize_also_known_as(aka) when is_binary(aka), do: [aka]
|
||||||
defp normalize_also_known_as(nil), do: []
|
defp normalize_also_known_as(nil), do: []
|
||||||
|
|
||||||
|
defp normalize_attachment(%{} = attachment), do: [attachment]
|
||||||
|
defp normalize_attachment(attachment) when is_list(attachment), do: attachment
|
||||||
|
defp normalize_attachment(_), do: []
|
||||||
|
|
||||||
defp object_to_user_data(data, additional) do
|
defp object_to_user_data(data, additional) do
|
||||||
fields =
|
fields =
|
||||||
data
|
data
|
||||||
|> Map.get("attachment", [])
|
|> Map.get("attachment", [])
|
||||||
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|
|> normalize_attachment()
|
||||||
|
|> Enum.filter(fn
|
||||||
|
%{"type" => t} -> t == "PropertyValue"
|
||||||
|
_ -> false
|
||||||
|
end)
|
||||||
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|
||||||
|
|
||||||
emojis =
|
emojis =
|
||||||
|
@ -1704,9 +1713,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
Fetcher.fetch_and_contain_remote_object_from_id(first) do
|
Fetcher.fetch_and_contain_remote_object_from_id(first) do
|
||||||
{:ok, false}
|
{:ok, false}
|
||||||
else
|
else
|
||||||
{:error, {:ok, %{status: code}}} when code in [401, 403] -> {:ok, true}
|
{:error, _} -> {:ok, true}
|
||||||
{:error, _} = e -> e
|
|
||||||
e -> {:error, e}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1722,6 +1729,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
|
|
||||||
def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) 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),
|
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, data} <- user_data_from_user_object(data, additional) do
|
||||||
{:ok, maybe_update_follow_information(data)}
|
{:ok, maybe_update_follow_information(data)}
|
||||||
else
|
else
|
||||||
|
@ -1730,10 +1738,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||||
{:error, e}
|
{:error, e}
|
||||||
|
|
||||||
{:error, {:reject, reason} = e} ->
|
{:reject, reason} = e ->
|
||||||
Logger.debug("Rejected user #{ap_id}: #{inspect(reason)}")
|
Logger.debug("Rejected user #{ap_id}: #{inspect(reason)}")
|
||||||
{:error, e}
|
{:error, e}
|
||||||
|
|
||||||
|
{:valid, reason} ->
|
||||||
|
Logger.debug("Data is not a valid user #{ap_id}: #{inspect(reason)}")
|
||||||
|
{:error, "Not a user"}
|
||||||
|
|
||||||
{:error, e} ->
|
{:error, e} ->
|
||||||
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||||
{:error, e}
|
{:error, e}
|
||||||
|
@ -1834,6 +1846,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) 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)
|
{: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
|
if user do
|
||||||
user
|
user
|
||||||
|> User.remote_user_changeset(data)
|
|> User.remote_user_changeset(data)
|
||||||
|
|
|
@ -12,9 +12,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||||
alias Pleroma.Web.ActivityPub.ObjectView
|
alias Pleroma.Web.ActivityPub.ObjectView
|
||||||
alias Pleroma.Web.ActivityPub.Pipeline
|
|
||||||
alias Pleroma.Web.ActivityPub.Relay
|
alias Pleroma.Web.ActivityPub.Relay
|
||||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
|
||||||
alias Pleroma.Web.ActivityPub.UserView
|
alias Pleroma.Web.ActivityPub.UserView
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
alias Pleroma.Web.ActivityPub.Visibility
|
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)
|
# Note: :following and :followers must be served even without authentication (as via :api)
|
||||||
plug(
|
plug(
|
||||||
EnsureAuthenticatedPlug
|
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(
|
plug(
|
||||||
Pleroma.Web.Plugs.Cache,
|
Pleroma.Web.Plugs.Cache,
|
||||||
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
|
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
|
||||||
|
@ -160,7 +156,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /relay/following
|
@doc """
|
||||||
|
GET /relay/following
|
||||||
|
"""
|
||||||
def relay_following(conn, _params) do
|
def relay_following(conn, _params) do
|
||||||
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
||||||
conn
|
conn
|
||||||
|
@ -197,7 +195,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /relay/followers
|
@doc """
|
||||||
|
GET /relay/followers
|
||||||
|
"""
|
||||||
def relay_followers(conn, _params) do
|
def relay_followers(conn, _params) do
|
||||||
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
||||||
conn
|
conn
|
||||||
|
@ -317,14 +317,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
|> represent_service_actor(conn)
|
|> represent_service_actor(conn)
|
||||||
end
|
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(
|
def read_inbox(
|
||||||
%{assigns: %{user: %User{nickname: nickname} = user}} = conn,
|
%{assigns: %{user: %User{nickname: nickname} = user}} = conn,
|
||||||
%{"nickname" => nickname, "page" => page?} = params
|
%{"nickname" => nickname, "page" => page?} = params
|
||||||
|
@ -375,105 +367,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
|> json(err)
|
|> json(err)
|
||||||
end
|
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
|
defp errors(conn, {:error, :not_found}) do
|
||||||
conn
|
conn
|
||||||
|> put_status(:not_found)
|
|> put_status(:not_found)
|
||||||
|
@ -495,21 +388,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
||||||
conn
|
conn
|
||||||
end
|
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
|
def pinned(conn, %{"nickname" => nickname}) do
|
||||||
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
conn
|
conn
|
||||||
|
|
|
@ -6,14 +6,29 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
|
||||||
@moduledoc "Force a quote line into the message content."
|
@moduledoc "Force a quote line into the message content."
|
||||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||||
|
|
||||||
|
alias Pleroma.Object
|
||||||
|
|
||||||
defp build_inline_quote(prefix, url) do
|
defp build_inline_quote(prefix, url) do
|
||||||
"<span class=\"quote-inline\"><br/><br/>#{prefix}: <a href=\"#{url}\">#{url}</a></span>"
|
"<span class=\"quote-inline\"><br/><br/>#{prefix}: <a href=\"#{url}\">#{url}</a></span>"
|
||||||
end
|
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
|
cond do
|
||||||
# Does the quote URL exist in the content?
|
# 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?
|
# Does the content already have a .quote-inline span?
|
||||||
content =~ "<span class=\"quote-inline\">" -> true
|
content =~ "<span class=\"quote-inline\">" -> true
|
||||||
# No inline quote found
|
# No inline quote found
|
||||||
|
@ -22,18 +37,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filter_object(%{"quoteUri" => quote_url} = object) do
|
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"] || ""
|
content = object["content"] || ""
|
||||||
|
|
||||||
if has_inline_quote?(content, quote_url) do
|
if has_inline_quote?(content, all_urls) do
|
||||||
object
|
object
|
||||||
else
|
else
|
||||||
prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix])
|
prefix = Pleroma.Config.get([:mrf_inline_quote, :prefix])
|
||||||
|
|
||||||
content =
|
content =
|
||||||
if String.ends_with?(content, "</p>") do
|
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
|
else
|
||||||
content <> build_inline_quote(prefix, quote_url)
|
content <> build_inline_quote(prefix, preferred_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
Map.put(object, "content", content)
|
Map.put(object, "content", content)
|
||||||
|
|
|
@ -6,10 +6,54 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias Pleroma.Config
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Emoji.Pack
|
||||||
|
|
||||||
@moduledoc "Detect new emojis by their shortcode and steals them"
|
@moduledoc "Detect new emojis by their shortcode and steals them"
|
||||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
@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 accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
|
||||||
|
|
||||||
defp shortcode_matches?(shortcode, pattern) when is_binary(pattern) do
|
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)
|
String.match?(shortcode, pattern)
|
||||||
end
|
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)
|
url = Pleroma.Web.MediaProxy.url(url)
|
||||||
|
|
||||||
with {:ok, %{status: status} = response} when status in 200..299 <- Pleroma.HTTP.get(url) do
|
with {:remote_size, true} <- {:remote_size, is_remote_size_within_limit?(url)},
|
||||||
size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
|
{: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
|
if byte_size(response.body) <= size_limit and extension do
|
||||||
extension =
|
steal_emoji(response, {shortcode, 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
|
|
||||||
else
|
else
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
"MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{size_limit} B)"
|
"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
|
if host != Pleroma.Web.Endpoint.host() and accept_host?(host) do
|
||||||
installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
|
installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
|
||||||
|
|
||||||
emoji_dir_path =
|
|
||||||
Config.get(
|
|
||||||
[:mrf_steal_emoji, :path],
|
|
||||||
Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
|
|
||||||
)
|
|
||||||
|
|
||||||
File.mkdir_p(emoji_dir_path)
|
|
||||||
|
|
||||||
new_emojis =
|
new_emojis =
|
||||||
foreign_emojis
|
foreign_emojis
|
||||||
|> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end)
|
|> Enum.reject(&reject_emoji?(&1, installed_emoji))
|
||||||
|> Enum.filter(fn {shortcode, _url} ->
|
|> Enum.map(&maybe_steal_emoji(&1))
|
||||||
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.filter(& &1)
|
|> Enum.filter(& &1)
|
||||||
|
|
||||||
if !Enum.empty?(new_emojis) do
|
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_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_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_url(data), do: data
|
||||||
|
|
||||||
defp fix_tag(%{"tag" => tag} = data) when is_list(tag) do
|
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.Emoji
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||||
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
|
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||||
|
@ -52,6 +53,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
||||||
defp fix(data) do
|
defp fix(data) do
|
||||||
data =
|
data =
|
||||||
data
|
data
|
||||||
|
|> Transmogrifier.fix_tag()
|
||||||
|> fix_emoji_qualification()
|
|> fix_emoji_qualification()
|
||||||
|> CommonFixes.fix_actor()
|
|> CommonFixes.fix_actor()
|
||||||
|> CommonFixes.fix_activity_addressing()
|
|> 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
|
|
@ -25,8 +25,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
require Logger
|
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
require Logger
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Modifies an incoming AP object (mastodon format) to our internal format.
|
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_summary(object), do: Map.put(object, "summary", "")
|
||||||
|
|
||||||
def fix_addressing_list(map, field) do
|
defp fix_addressing_list(addrs) do
|
||||||
addrs = map[field]
|
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
is_list(addrs) ->
|
is_list(addrs) -> Enum.filter(addrs, &is_binary/1)
|
||||||
Map.put(map, field, Enum.filter(addrs, &is_binary/1))
|
is_binary(addrs) -> [addrs]
|
||||||
|
true -> []
|
||||||
is_binary(addrs) ->
|
|
||||||
Map.put(map, field, [addrs])
|
|
||||||
|
|
||||||
true ->
|
|
||||||
Map.put(map, field, [])
|
|
||||||
end
|
end
|
||||||
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
|
# if directMessage flag is set to true, leave the addressing alone
|
||||||
def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection),
|
def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection),
|
||||||
do: object
|
do: object
|
||||||
|
@ -96,6 +123,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
|> Map.put("cc", final_cc)
|
|> Map.put("cc", final_cc)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fix_addressing_list_key(map, field) do
|
||||||
|
Map.put(map, field, fix_addressing_list(map[field]))
|
||||||
|
end
|
||||||
|
|
||||||
def fix_addressing(object) do
|
def fix_addressing(object) do
|
||||||
{:ok, %User{follower_address: follower_collection}} =
|
{:ok, %User{follower_address: follower_collection}} =
|
||||||
object
|
object
|
||||||
|
@ -103,10 +134,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
|> User.get_or_fetch_by_ap_id()
|
|> User.get_or_fetch_by_ap_id()
|
||||||
|
|
||||||
object
|
object
|
||||||
|> fix_addressing_list("to")
|
|> fix_addressing_list_key("to")
|
||||||
|> fix_addressing_list("cc")
|
|> fix_addressing_list_key("cc")
|
||||||
|> fix_addressing_list("bto")
|
|> fix_addressing_list_key("bto")
|
||||||
|> fix_addressing_list("bcc")
|
|> fix_addressing_list_key("bcc")
|
||||||
|> fix_explicit_addressing(follower_collection)
|
|> fix_explicit_addressing(follower_collection)
|
||||||
|> CommonFixes.fix_implicit_addressing(follower_collection)
|
|> CommonFixes.fix_implicit_addressing(follower_collection)
|
||||||
end
|
end
|
||||||
|
@ -135,8 +166,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
|> Map.put("context", replied_object.data["context"] || object["conversation"])
|
|> Map.put("context", replied_object.data["context"] || object["conversation"])
|
||||||
|> Map.drop(["conversation", "inReplyToAtomUri"])
|
|> Map.drop(["conversation", "inReplyToAtomUri"])
|
||||||
else
|
else
|
||||||
e ->
|
_ ->
|
||||||
Logger.warning("Couldn't fetch reply@#{inspect(in_reply_to_id)}, error: #{inspect(e)}")
|
|
||||||
object
|
object
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
@ -384,11 +414,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end)
|
end)
|
||||||
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
|
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
|
||||||
# with nil ID.
|
# 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(),
|
with context <- data["context"] || Utils.generate_context_id(),
|
||||||
content <- data["content"] || "",
|
content <- data["content"] || "",
|
||||||
%User{} = actor <- User.get_cached_by_ap_id(actor),
|
%User{} = actor <- User.get_cached_by_ap_id(actor),
|
||||||
|
@ -409,20 +456,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
|
|
||||||
# disallow objects with bogus IDs
|
# disallow objects with bogus IDs
|
||||||
def handle_incoming(%{"id" => nil}, _options), do: :error
|
defp handle_incoming_normalised(%{"id" => nil}, _options), do: :error
|
||||||
def handle_incoming(%{"id" => ""}, _options), do: :error
|
defp handle_incoming_normalised(%{"id" => ""}, _options), do: :error
|
||||||
# length of https:// = 8, should validate better, but good enough for now.
|
# 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,
|
defp handle_incoming_normalised(%{"id" => id}, _options)
|
||||||
do: :error
|
when is_binary(id) and byte_size(id) < 8,
|
||||||
|
do: :error
|
||||||
|
|
||||||
@doc "Rewrite misskey likes into EmojiReacts"
|
# Rewrite misskey likes into EmojiReacts
|
||||||
def handle_incoming(
|
defp handle_incoming_normalised(
|
||||||
%{
|
%{
|
||||||
"type" => "Like",
|
"type" => "Like",
|
||||||
"content" => reaction
|
"content" => reaction
|
||||||
} = data,
|
} = data,
|
||||||
options
|
options
|
||||||
) do
|
) do
|
||||||
if Pleroma.Emoji.is_unicode_emoji?(reaction) || Pleroma.Emoji.matches_shortcode?(reaction) do
|
if Pleroma.Emoji.is_unicode_emoji?(reaction) || Pleroma.Emoji.matches_shortcode?(reaction) do
|
||||||
data
|
data
|
||||||
|> Map.put("type", "EmojiReact")
|
|> Map.put("type", "EmojiReact")
|
||||||
|
@ -434,11 +482,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
defp handle_incoming_normalised(
|
||||||
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
|
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
when objtype in ~w{Question Answer Audio Video Event Article Note Page} do
|
when objtype in ~w{Question Answer Audio Video Event Article Note Page} do
|
||||||
fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
|
fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
|
||||||
|
|
||||||
object =
|
object =
|
||||||
|
@ -470,8 +518,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(%{"type" => type} = data, _options)
|
defp handle_incoming_normalised(%{"type" => type} = data, _options)
|
||||||
when type in ~w{Like EmojiReact Announce Add Remove} do
|
when type in ~w{Like EmojiReact Announce Add Remove} do
|
||||||
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
||||||
{:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do
|
{:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
|
@ -481,11 +529,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
defp handle_incoming_normalised(
|
||||||
%{"type" => type} = data,
|
%{"type" => type} = data,
|
||||||
_options
|
_options
|
||||||
)
|
)
|
||||||
when type in ~w{Update Block Follow Accept Reject} do
|
when type in ~w{Update Block Follow Accept Reject} do
|
||||||
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
|
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
|
||||||
{:ok, activity, _} <-
|
{:ok, activity, _} <-
|
||||||
Pipeline.common_pipeline(data, local: false) do
|
Pipeline.common_pipeline(data, local: false) do
|
||||||
|
@ -493,10 +541,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
defp handle_incoming_normalised(
|
||||||
%{"type" => "Delete"} = data,
|
%{"type" => "Delete"} = data,
|
||||||
_options
|
_options
|
||||||
) do
|
) do
|
||||||
with {:ok, activity, _} <-
|
with {:ok, activity, _} <-
|
||||||
Pipeline.common_pipeline(data, local: false) do
|
Pipeline.common_pipeline(data, local: false) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
|
@ -516,15 +564,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
defp handle_incoming_normalised(
|
||||||
%{
|
%{
|
||||||
"type" => "Undo",
|
"type" => "Undo",
|
||||||
"object" => %{"type" => "Follow", "object" => followed},
|
"object" => %{"type" => "Follow", "object" => followed},
|
||||||
"actor" => follower,
|
"actor" => follower,
|
||||||
"id" => id
|
"id" => id
|
||||||
} = _data,
|
} = _data,
|
||||||
_options
|
_options
|
||||||
) do
|
) do
|
||||||
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
|
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
|
||||||
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
|
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
|
||||||
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
|
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
|
||||||
|
@ -535,28 +583,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
defp handle_incoming_normalised(
|
||||||
%{
|
%{
|
||||||
"type" => "Undo",
|
"type" => "Undo",
|
||||||
"object" => %{"type" => type}
|
"object" => %{"type" => type}
|
||||||
} = data,
|
} = data,
|
||||||
_options
|
_options
|
||||||
)
|
)
|
||||||
when type in ["Like", "EmojiReact", "Announce", "Block"] do
|
when type in ["Like", "EmojiReact", "Announce", "Block"] do
|
||||||
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||||
{:ok, activity}
|
{:ok, activity}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# For Undos that don't have the complete object attached, try to find it in our database.
|
# For Undos that don't have the complete object attached, try to find it in our database.
|
||||||
def handle_incoming(
|
defp handle_incoming_normalised(
|
||||||
%{
|
%{
|
||||||
"type" => "Undo",
|
"type" => "Undo",
|
||||||
"object" => object
|
"object" => object
|
||||||
} = activity,
|
} = activity,
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
when is_binary(object) do
|
when is_binary(object) do
|
||||||
with %Activity{data: data} <- Activity.get_by_ap_id(object) do
|
with %Activity{data: data} <- Activity.get_by_ap_id(object) do
|
||||||
activity
|
activity
|
||||||
|> Map.put("object", data)
|
|> Map.put("object", data)
|
||||||
|
@ -566,17 +614,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_incoming(
|
defp handle_incoming_normalised(
|
||||||
%{
|
%{
|
||||||
"type" => "Move",
|
"type" => "Move",
|
||||||
"actor" => origin_actor,
|
"actor" => origin_actor,
|
||||||
"object" => origin_actor,
|
"object" => origin_actor,
|
||||||
"target" => target_actor
|
"target" => target_actor
|
||||||
},
|
},
|
||||||
_options
|
_options
|
||||||
) do
|
) do
|
||||||
with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
|
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
|
true <- origin_actor in target_user.also_known_as do
|
||||||
ActivityPub.move(origin_user, target_user, false)
|
ActivityPub.move(origin_user, target_user, false)
|
||||||
else
|
else
|
||||||
|
@ -584,7 +637,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
end
|
end
|
||||||
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
|
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
|
||||||
def get_obj_helper(id, options \\ []) do
|
def get_obj_helper(id, options \\ []) do
|
||||||
|
@ -828,8 +881,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
||||||
relative_object do
|
relative_object do
|
||||||
Map.put(data, "object", external_url)
|
Map.put(data, "object", external_url)
|
||||||
else
|
else
|
||||||
{:fetch, e} ->
|
{:fetch, _} ->
|
||||||
Logger.error("Couldn't fetch fixed_object@#{object} #{inspect(e)}")
|
|
||||||
data
|
data
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
|
|
|
@ -26,8 +26,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
||||||
"oauthAuthorizationEndpoint" => url(~p"/oauth/authorize"),
|
"oauthAuthorizationEndpoint" => url(~p"/oauth/authorize"),
|
||||||
"oauthRegistrationEndpoint" => url(~p"/api/v1/apps"),
|
"oauthRegistrationEndpoint" => url(~p"/api/v1/apps"),
|
||||||
"oauthTokenEndpoint" => url(~p"/oauth/token"),
|
"oauthTokenEndpoint" => url(~p"/oauth/token"),
|
||||||
"sharedInbox" => url(~p"/inbox"),
|
"sharedInbox" => url(~p"/inbox")
|
||||||
"uploadMedia" => url(~p"/api/ap/upload_media")
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
||||||
"removes the contents of a message from the push notification"
|
"removes the contents of a message from the push notification"
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
requestBody: nil,
|
requestBody: request_body("Parameters", update_notification_settings_request()),
|
||||||
responses: %{
|
responses: %{
|
||||||
200 =>
|
200 =>
|
||||||
Operation.response("Success", "application/json", %Schema{
|
Operation.response("Success", "application/json", %Schema{
|
||||||
|
@ -432,4 +432,22 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -41,7 +41,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
preview?: false,
|
preview?: false,
|
||||||
changes: %{}
|
changes: %{}
|
||||||
|
|
||||||
def new(user, params) do
|
defp new(user, params) do
|
||||||
%__MODULE__{user: user}
|
%__MODULE__{user: user}
|
||||||
|> put_params(params)
|
|> put_params(params)
|
||||||
end
|
end
|
||||||
|
@ -92,9 +92,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp attachments(%{params: params} = draft) do
|
defp attachments(%{params: params, user: user} = draft) do
|
||||||
attachments = Utils.attachments_from_ids(params)
|
case Utils.attachments_from_ids(user, params) do
|
||||||
%__MODULE__{draft | attachments: attachments}
|
attachments when is_list(attachments) ->
|
||||||
|
%__MODULE__{draft | attachments: attachments}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
add_error(draft, reason)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
|
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
|
||||||
|
|
|
@ -22,43 +22,31 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
||||||
require Logger
|
require Logger
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
|
||||||
def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do
|
def attachments_from_ids(user, %{media_ids: ids}) do
|
||||||
attachments_from_ids_descs(ids, desc)
|
attachments_from_ids(user, ids, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def attachments_from_ids(%{media_ids: ids}) do
|
def attachments_from_ids(_, _), do: []
|
||||||
attachments_from_ids_no_descs(ids)
|
|
||||||
|
defp attachments_from_ids(_user, [], acc), do: Enum.reverse(acc)
|
||||||
|
|
||||||
|
defp attachments_from_ids(user, [media_id | ids], acc) do
|
||||||
|
with {_, %Object{} = object} <- {:get, get_attachment(media_id)},
|
||||||
|
:ok <- Object.authorize_access(object, user) do
|
||||||
|
attachments_from_ids(user, ids, [object.data | acc])
|
||||||
|
else
|
||||||
|
{:get, _} -> attachments_from_ids(user, ids, acc)
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def attachments_from_ids(_), do: []
|
def get_attachment(media_id) do
|
||||||
|
with %Object{} = object <- Repo.get(Object, media_id),
|
||||||
def attachments_from_ids_no_descs([]), do: []
|
true <- object.data["type"] in Pleroma.Constants.attachment_types() do
|
||||||
|
object
|
||||||
def attachments_from_ids_no_descs(ids) do
|
else
|
||||||
Enum.map(ids, fn media_id ->
|
_ -> nil
|
||||||
case get_attachment(media_id) do
|
end
|
||||||
%Object{data: data} -> data
|
|
||||||
_ -> nil
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|> Enum.reject(&is_nil/1)
|
|
||||||
end
|
|
||||||
|
|
||||||
def attachments_from_ids_descs([], _), do: []
|
|
||||||
|
|
||||||
def attachments_from_ids_descs(ids, descs_str) do
|
|
||||||
{_, descs} = Jason.decode(descs_str)
|
|
||||||
|
|
||||||
Enum.map(ids, fn media_id ->
|
|
||||||
with %Object{data: data} <- get_attachment(media_id) do
|
|
||||||
Map.put(data, "name", descs[media_id])
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|> Enum.reject(&is_nil/1)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_attachment(media_id) do
|
|
||||||
Repo.get(Object, media_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
|
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
|
||||||
|
|
|
@ -98,6 +98,10 @@ defmodule Pleroma.Web.Endpoint do
|
||||||
at: "/",
|
at: "/",
|
||||||
from: :pleroma,
|
from: :pleroma,
|
||||||
only: Pleroma.Web.static_paths(),
|
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
|
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
||||||
gzip: true,
|
gzip: true,
|
||||||
cache_control_for_etags: @static_cache_control,
|
cache_control_for_etags: @static_cache_control,
|
||||||
|
|
|
@ -78,9 +78,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|
||||||
end
|
end
|
||||||
|
|
||||||
def activity_content(%{"content" => content}) do
|
def activity_content(%{"content" => content}) do
|
||||||
content
|
escape(content)
|
||||||
|> String.replace(~r/[\n\r]/, "")
|
|
||||||
|> escape()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def activity_content(_), do: ""
|
def activity_content(_), do: ""
|
||||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
|
alias Pleroma.Web.CommonAPI.Utils
|
||||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||||
|
|
||||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||||
|
@ -55,12 +56,15 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
|
||||||
|
|
||||||
@doc "PUT /api/v1/media/:id"
|
@doc "PUT /api/v1/media/:id"
|
||||||
def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do
|
def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do
|
||||||
with %Object{} = object <- Object.get_by_id(id),
|
with {_, %Object{} = object} <- {:get, Utils.get_attachment(id)},
|
||||||
:ok <- Object.authorize_access(object, user),
|
:ok <- Object.authorize_access(object, user),
|
||||||
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
|
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
|
||||||
attachment_data = Map.put(data, "id", object.id)
|
attachment_data = Map.put(data, "id", object.id)
|
||||||
|
|
||||||
render(conn, "attachment.json", %{attachment: attachment_data})
|
render(conn, "attachment.json", %{attachment: attachment_data})
|
||||||
|
else
|
||||||
|
{:get, _} -> {:error, :not_found}
|
||||||
|
e -> e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -68,11 +72,14 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
|
||||||
|
|
||||||
@doc "GET /api/v1/media/:id"
|
@doc "GET /api/v1/media/:id"
|
||||||
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
|
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
|
||||||
with %Object{data: data, id: object_id} = object <- Object.get_by_id(id),
|
with {_, %Object{data: data, id: object_id} = object} <- {:get, Utils.get_attachment(id)},
|
||||||
:ok <- Object.authorize_access(object, user) do
|
:ok <- Object.authorize_access(object, user) do
|
||||||
attachment_data = Map.put(data, "id", object_id)
|
attachment_data = Map.put(data, "id", object_id)
|
||||||
|
|
||||||
render(conn, "attachment.json", %{attachment: attachment_data})
|
render(conn, "attachment.json", %{attachment: attachment_data})
|
||||||
|
else
|
||||||
|
{:get, _} -> {:error, :not_found}
|
||||||
|
e -> e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
||||||
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
|
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
|
||||||
)
|
)
|
||||||
|
|
||||||
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
|
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete update)a
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
RateLimiter,
|
RateLimiter,
|
||||||
|
|
|
@ -145,7 +145,7 @@ defmodule Pleroma.Web.MediaProxy do
|
||||||
end
|
end
|
||||||
|
|
||||||
def base_url do
|
def base_url do
|
||||||
Config.get([:media_proxy, :base_url], Endpoint.url())
|
Config.get!([:media_proxy, :base_url])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp proxy_url(path, sig_base64, url_base64, filename) do
|
defp proxy_url(path, sig_base64, url_base64, filename) do
|
||||||
|
|
|
@ -12,14 +12,38 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
|
||||||
@behaviour Provider
|
@behaviour Provider
|
||||||
@media_types ["image", "audio", "video"]
|
@media_types ["image", "audio", "video"]
|
||||||
|
|
||||||
|
defp user_avatar_tags(user) do
|
||||||
|
if Utils.visible?(user) do
|
||||||
|
[
|
||||||
|
{:meta, [property: "og:image", content: MediaProxy.preview_url(User.avatar_url(user))],
|
||||||
|
[]},
|
||||||
|
{:meta, [property: "og:image:width", content: 150], []},
|
||||||
|
{:meta, [property: "og:image:height", content: 150], []}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@impl Provider
|
@impl Provider
|
||||||
def build_tags(%{
|
def build_tags(%{
|
||||||
object: object,
|
object: object,
|
||||||
url: url,
|
url: url,
|
||||||
user: user
|
user: user
|
||||||
}) do
|
}) do
|
||||||
attachments = build_attachments(object)
|
attachments =
|
||||||
scrubbed_content = Utils.scrub_html_and_truncate(object)
|
if Utils.visible?(object) do
|
||||||
|
build_attachments(object)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
scrubbed_content =
|
||||||
|
if Utils.visible?(object) do
|
||||||
|
Utils.scrub_html_and_truncate(object)
|
||||||
|
else
|
||||||
|
"Content cannot be displayed."
|
||||||
|
end
|
||||||
|
|
||||||
[
|
[
|
||||||
{:meta,
|
{:meta,
|
||||||
|
@ -36,12 +60,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
|
||||||
{:meta, [property: "og:type", content: "article"], []}
|
{:meta, [property: "og:type", content: "article"], []}
|
||||||
] ++
|
] ++
|
||||||
if attachments == [] or Metadata.activity_nsfw?(object) do
|
if attachments == [] or Metadata.activity_nsfw?(object) do
|
||||||
[
|
user_avatar_tags(user)
|
||||||
{:meta, [property: "og:image", content: MediaProxy.preview_url(User.avatar_url(user))],
|
|
||||||
[]},
|
|
||||||
{:meta, [property: "og:image:width", content: 150], []},
|
|
||||||
{:meta, [property: "og:image:height", content: 150], []}
|
|
||||||
]
|
|
||||||
else
|
else
|
||||||
attachments
|
attachments
|
||||||
end
|
end
|
||||||
|
@ -49,7 +68,9 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
|
||||||
|
|
||||||
@impl Provider
|
@impl Provider
|
||||||
def build_tags(%{user: user}) do
|
def build_tags(%{user: user}) do
|
||||||
with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do
|
if Utils.visible?(user) do
|
||||||
|
truncated_bio = Utils.scrub_html_and_truncate(user.bio)
|
||||||
|
|
||||||
[
|
[
|
||||||
{:meta,
|
{:meta,
|
||||||
[
|
[
|
||||||
|
@ -58,12 +79,10 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
|
||||||
], []},
|
], []},
|
||||||
{:meta, [property: "og:url", content: user.uri || user.ap_id], []},
|
{:meta, [property: "og:url", content: user.uri || user.ap_id], []},
|
||||||
{:meta, [property: "og:description", content: truncated_bio], []},
|
{:meta, [property: "og:description", content: truncated_bio], []},
|
||||||
{:meta, [property: "og:type", content: "article"], []},
|
{:meta, [property: "og:type", content: "article"], []}
|
||||||
{:meta, [property: "og:image", content: MediaProxy.preview_url(User.avatar_url(user))],
|
] ++ user_avatar_tags(user)
|
||||||
[]},
|
else
|
||||||
{:meta, [property: "og:image:width", content: 150], []},
|
[]
|
||||||
{:meta, [property: "og:image:height", content: 150], []}
|
|
||||||
]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,19 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
|
||||||
|
|
||||||
@impl Provider
|
@impl Provider
|
||||||
def build_tags(%{activity_id: id, object: object, user: user}) do
|
def build_tags(%{activity_id: id, object: object, user: user}) do
|
||||||
attachments = build_attachments(id, object)
|
attachments =
|
||||||
scrubbed_content = Utils.scrub_html_and_truncate(object)
|
if Utils.visible?(object) do
|
||||||
|
build_attachments(id, object)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
scrubbed_content =
|
||||||
|
if Utils.visible?(object) do
|
||||||
|
Utils.scrub_html_and_truncate(object)
|
||||||
|
else
|
||||||
|
"Content cannot be displayed."
|
||||||
|
end
|
||||||
|
|
||||||
[
|
[
|
||||||
title_tag(user),
|
title_tag(user),
|
||||||
|
@ -36,13 +47,17 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
|
||||||
|
|
||||||
@impl Provider
|
@impl Provider
|
||||||
def build_tags(%{user: user}) do
|
def build_tags(%{user: user}) do
|
||||||
with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do
|
if Utils.visible?(user) do
|
||||||
[
|
with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do
|
||||||
title_tag(user),
|
[
|
||||||
{:meta, [name: "twitter:description", content: truncated_bio], []},
|
title_tag(user),
|
||||||
image_tag(user),
|
{:meta, [name: "twitter:description", content: truncated_bio], []},
|
||||||
{:meta, [name: "twitter:card", content: "summary"], []}
|
image_tag(user),
|
||||||
]
|
{:meta, [name: "twitter:card", content: "summary"], []}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -51,7 +66,11 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
|
||||||
end
|
end
|
||||||
|
|
||||||
def image_tag(user) do
|
def image_tag(user) do
|
||||||
{:meta, [name: "twitter:image", content: MediaProxy.preview_url(User.avatar_url(user))], []}
|
if Utils.visible?(user) do
|
||||||
|
{:meta, [name: "twitter:image", content: MediaProxy.preview_url(User.avatar_url(user))], []}
|
||||||
|
else
|
||||||
|
{:meta, [name: "twitter:image", content: ""], []}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_attachments(id, %{data: %{"attachment" => attachments}}) do
|
defp build_attachments(id, %{data: %{"attachment" => attachments}}) do
|
||||||
|
|
|
@ -7,6 +7,15 @@ defmodule Pleroma.Web.Metadata.Utils do
|
||||||
alias Pleroma.Emoji
|
alias Pleroma.Emoji
|
||||||
alias Pleroma.Formatter
|
alias Pleroma.Formatter
|
||||||
alias Pleroma.HTML
|
alias Pleroma.HTML
|
||||||
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
|
|
||||||
|
def visible?(%Pleroma.User{} = object) do
|
||||||
|
Visibility.restrict_unauthenticated_access?(object) == :visible
|
||||||
|
end
|
||||||
|
|
||||||
|
def visible?(object) do
|
||||||
|
Visibility.visible_for_user?(object, nil)
|
||||||
|
end
|
||||||
|
|
||||||
defp scrub_html_and_truncate_object_field(field, object) do
|
defp scrub_html_and_truncate_object_field(field, object) do
|
||||||
field
|
field
|
||||||
|
|
|
@ -3,8 +3,12 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Web.Plugs.InstanceStatic do
|
defmodule Pleroma.Web.Plugs.InstanceStatic do
|
||||||
|
import Plug.Conn
|
||||||
|
|
||||||
require Pleroma.Constants
|
require Pleroma.Constants
|
||||||
|
|
||||||
|
alias Pleroma.Web.Plugs.Utils
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
This is a shim to call `Plug.Static` but with runtime `from` configuration.
|
This is a shim to call `Plug.Static` but with runtime `from` configuration.
|
||||||
|
|
||||||
|
@ -43,11 +47,25 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do
|
||||||
conn
|
conn
|
||||||
end
|
end
|
||||||
|
|
||||||
defp call_static(conn, opts, from) do
|
defp set_static_content_type(conn, "/emoji/" <> _ = request_path) do
|
||||||
|
real_mime = MIME.from_path(request_path)
|
||||||
|
safe_mime = Utils.get_safe_mime_type(%{allowed_mime_types: ["image"]}, real_mime)
|
||||||
|
|
||||||
|
put_resp_header(conn, "content-type", safe_mime)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp set_static_content_type(conn, request_path) do
|
||||||
|
put_resp_header(conn, "content-type", MIME.from_path(request_path))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp call_static(%{request_path: request_path} = conn, opts, from) do
|
||||||
opts =
|
opts =
|
||||||
opts
|
opts
|
||||||
|> Map.put(:from, from)
|
|> Map.put(:from, from)
|
||||||
|
|> Map.put(:set_content_type, false)
|
||||||
|
|
||||||
Plug.Static.call(conn, opts)
|
conn
|
||||||
|
|> set_static_content_type(request_path)
|
||||||
|
|> Pleroma.Web.Plugs.StaticNoCT.call(opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
469
lib/pleroma/web/plugs/static_no_content_type.ex
Normal file
469
lib/pleroma/web/plugs/static_no_content_type.ex
Normal file
|
@ -0,0 +1,469 @@
|
||||||
|
# This is almost identical to Plug.Static from Plug 1.15.3 (2024-01-16)
|
||||||
|
# It being copied is a temporary measure to fix an urgent bug without
|
||||||
|
# needing to wait for merge of a suitable patch upstream
|
||||||
|
# The differences are:
|
||||||
|
# - this leading comment
|
||||||
|
# - renaming of the module from 'Plug.Static' to 'Pleroma.Web.Plugs.StaticNoCT'
|
||||||
|
# - additon of set_content_type option
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.Plugs.StaticNoCT do
|
||||||
|
@moduledoc """
|
||||||
|
A plug for serving static assets.
|
||||||
|
|
||||||
|
It requires two options:
|
||||||
|
|
||||||
|
* `:at` - the request path to reach for static assets.
|
||||||
|
It must be a string.
|
||||||
|
|
||||||
|
* `:from` - the file system path to read static assets from.
|
||||||
|
It can be either: a string containing a file system path, an
|
||||||
|
atom representing the application name (where assets will
|
||||||
|
be served from `priv/static`), a tuple containing the
|
||||||
|
application name and the directory to serve assets from (besides
|
||||||
|
`priv/static`), or an MFA tuple.
|
||||||
|
|
||||||
|
The preferred form is to use `:from` with an atom or tuple, since
|
||||||
|
it will make your application independent from the starting directory.
|
||||||
|
For example, if you pass:
|
||||||
|
|
||||||
|
plug Plug.Static, from: "priv/app/path"
|
||||||
|
|
||||||
|
Plug.Static will be unable to serve assets if you build releases
|
||||||
|
or if you change the current directory. Instead do:
|
||||||
|
|
||||||
|
plug Plug.Static, from: {:app_name, "priv/app/path"}
|
||||||
|
|
||||||
|
If a static asset cannot be found, `Plug.Static` simply forwards
|
||||||
|
the connection to the rest of the pipeline.
|
||||||
|
|
||||||
|
## Cache mechanisms
|
||||||
|
|
||||||
|
`Plug.Static` uses etags for HTTP caching. This means browsers/clients
|
||||||
|
should cache assets on the first request and validate the cache on
|
||||||
|
following requests, not downloading the static asset once again if it
|
||||||
|
has not changed. The cache-control for etags is specified by the
|
||||||
|
`cache_control_for_etags` option and defaults to `"public"`.
|
||||||
|
|
||||||
|
However, `Plug.Static` also supports direct cache control by using
|
||||||
|
versioned query strings. If the request query string starts with
|
||||||
|
"?vsn=", `Plug.Static` assumes the application is versioning assets
|
||||||
|
and does not set the `ETag` header, meaning the cache behaviour will
|
||||||
|
be specified solely by the `cache_control_for_vsn_requests` config,
|
||||||
|
which defaults to `"public, max-age=31536000"`.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
* `:encodings` - list of 2-ary tuples where first value is value of
|
||||||
|
the `Accept-Encoding` header and second is extension of the file to
|
||||||
|
be served if given encoding is accepted by client. Entries will be tested
|
||||||
|
in order in list, so entries higher in list will be preferred. Defaults
|
||||||
|
to: `[]`.
|
||||||
|
|
||||||
|
In addition to setting this value directly it supports 2 additional
|
||||||
|
options for compatibility reasons:
|
||||||
|
|
||||||
|
+ `:brotli` - will append `{"br", ".br"}` to the encodings list.
|
||||||
|
+ `:gzip` - will append `{"gzip", ".gz"}` to the encodings list.
|
||||||
|
|
||||||
|
Additional options will be added in the above order (Brotli takes
|
||||||
|
preference over Gzip) to reflect older behaviour which was set due
|
||||||
|
to fact that Brotli in general provides better compression ratio than
|
||||||
|
Gzip.
|
||||||
|
|
||||||
|
* `:cache_control_for_etags` - sets the cache header for requests
|
||||||
|
that use etags. Defaults to `"public"`.
|
||||||
|
|
||||||
|
* `:etag_generation` - specify a `{module, function, args}` to be used
|
||||||
|
to generate an etag. The `path` of the resource will be passed to
|
||||||
|
the function, as well as the `args`. If this option is not supplied,
|
||||||
|
etags will be generated based off of file size and modification time.
|
||||||
|
Note it is [recommended for the etag value to be quoted](https://tools.ietf.org/html/rfc7232#section-2.3),
|
||||||
|
which Plug won't do automatically.
|
||||||
|
|
||||||
|
* `:cache_control_for_vsn_requests` - sets the cache header for
|
||||||
|
requests starting with "?vsn=" in the query string. Defaults to
|
||||||
|
`"public, max-age=31536000"`.
|
||||||
|
|
||||||
|
* `:only` - filters which requests to serve. This is useful to avoid
|
||||||
|
file system access on every request when this plug is mounted
|
||||||
|
at `"/"`. For example, if `only: ["images", "favicon.ico"]` is
|
||||||
|
specified, only files in the "images" directory and the
|
||||||
|
"favicon.ico" file will be served by `Plug.Static`.
|
||||||
|
Note that `Plug.Static` matches these filters against request
|
||||||
|
uri and not against the filesystem. When requesting
|
||||||
|
a file with name containing non-ascii or special characters,
|
||||||
|
you should use urlencoded form. For example, you should write
|
||||||
|
`only: ["file%20name"]` instead of `only: ["file name"]`.
|
||||||
|
Defaults to `nil` (no filtering).
|
||||||
|
|
||||||
|
* `:only_matching` - a relaxed version of `:only` that will
|
||||||
|
serve any request as long as one of the given values matches the
|
||||||
|
given path. For example, `only_matching: ["images", "favicon"]`
|
||||||
|
will match any request that starts at "images" or "favicon",
|
||||||
|
be it "/images/foo.png", "/images-high/foo.png", "/favicon.ico"
|
||||||
|
or "/favicon-high.ico". Such matches are useful when serving
|
||||||
|
digested files at the root. Defaults to `nil` (no filtering).
|
||||||
|
|
||||||
|
* `:headers` - other headers to be set when serving static assets. Specify either
|
||||||
|
an enum of key-value pairs or a `{module, function, args}` to return an enum. The
|
||||||
|
`conn` will be passed to the function, as well as the `args`.
|
||||||
|
|
||||||
|
* `:content_types` - custom MIME type mapping. As a map with filename as key
|
||||||
|
and content type as value. For example:
|
||||||
|
`content_types: %{"apple-app-site-association" => "application/json"}`.
|
||||||
|
|
||||||
|
* `:set_content_type` - by default Plug.Static (re)sets the content type header
|
||||||
|
using auto-detection and the `:content_types` map. But when set to `false`
|
||||||
|
no content-type header will be inserted instead retaining the original
|
||||||
|
value or lack thereof. This can be useful when custom logic for appropiate
|
||||||
|
content types is needed which cannot be reasonably expressed as a static
|
||||||
|
filename map.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This plug can be mounted in a `Plug.Builder` pipeline as follows:
|
||||||
|
|
||||||
|
defmodule MyPlug do
|
||||||
|
use Plug.Builder
|
||||||
|
|
||||||
|
plug Plug.Static,
|
||||||
|
at: "/public",
|
||||||
|
from: :my_app,
|
||||||
|
only: ~w(images robots.txt)
|
||||||
|
plug :not_found
|
||||||
|
|
||||||
|
def not_found(conn, _) do
|
||||||
|
send_resp(conn, 404, "not found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@behaviour Plug
|
||||||
|
@allowed_methods ~w(GET HEAD)
|
||||||
|
|
||||||
|
import Plug.Conn
|
||||||
|
alias Plug.Conn
|
||||||
|
|
||||||
|
# In this module, the `:prim_file` Erlang module along with the `:file_info`
|
||||||
|
# record are used instead of the more common and Elixir-y `File` module and
|
||||||
|
# `File.Stat` struct, respectively. The reason behind this is performance: all
|
||||||
|
# the `File` operations pass through a single process in order to support node
|
||||||
|
# operations that we simply don't need when serving assets.
|
||||||
|
|
||||||
|
require Record
|
||||||
|
Record.defrecordp(:file_info, Record.extract(:file_info, from_lib: "kernel/include/file.hrl"))
|
||||||
|
|
||||||
|
defmodule InvalidPathError do
|
||||||
|
defexception message: "invalid path for static asset", plug_status: 400
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(opts) do
|
||||||
|
from =
|
||||||
|
case Keyword.fetch!(opts, :from) do
|
||||||
|
{_, _} = from -> from
|
||||||
|
{_, _, _} = from -> from
|
||||||
|
from when is_atom(from) -> {from, "priv/static"}
|
||||||
|
from when is_binary(from) -> from
|
||||||
|
_ -> raise ArgumentError, ":from must be an atom, a binary or a tuple"
|
||||||
|
end
|
||||||
|
|
||||||
|
encodings =
|
||||||
|
opts
|
||||||
|
|> Keyword.get(:encodings, [])
|
||||||
|
|> maybe_add("br", ".br", Keyword.get(opts, :brotli, false))
|
||||||
|
|> maybe_add("gzip", ".gz", Keyword.get(opts, :gzip, false))
|
||||||
|
|
||||||
|
%{
|
||||||
|
encodings: encodings,
|
||||||
|
only_rules: {Keyword.get(opts, :only, []), Keyword.get(opts, :only_matching, [])},
|
||||||
|
qs_cache: Keyword.get(opts, :cache_control_for_vsn_requests, "public, max-age=31536000"),
|
||||||
|
et_cache: Keyword.get(opts, :cache_control_for_etags, "public"),
|
||||||
|
et_generation: Keyword.get(opts, :etag_generation, nil),
|
||||||
|
headers: Keyword.get(opts, :headers, %{}),
|
||||||
|
content_types: Keyword.get(opts, :content_types, %{}),
|
||||||
|
set_content_type: Keyword.get(opts, :set_content_type, true),
|
||||||
|
from: from,
|
||||||
|
at: opts |> Keyword.fetch!(:at) |> Plug.Router.Utils.split()
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def call(
|
||||||
|
conn = %Conn{method: meth},
|
||||||
|
%{at: at, only_rules: only_rules, from: from, encodings: encodings} = options
|
||||||
|
)
|
||||||
|
when meth in @allowed_methods do
|
||||||
|
segments = subset(at, conn.path_info)
|
||||||
|
|
||||||
|
if allowed?(only_rules, segments) do
|
||||||
|
segments = Enum.map(segments, &uri_decode/1)
|
||||||
|
|
||||||
|
if invalid_path?(segments) do
|
||||||
|
raise InvalidPathError, "invalid path for static asset: #{conn.request_path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
path = path(from, segments)
|
||||||
|
range = get_req_header(conn, "range")
|
||||||
|
encoding = file_encoding(conn, path, range, encodings)
|
||||||
|
serve_static(encoding, conn, segments, range, options)
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(conn, _options) do
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
|
||||||
|
defp uri_decode(path) do
|
||||||
|
# TODO: Remove rescue as this can't fail from Elixir v1.13
|
||||||
|
try do
|
||||||
|
URI.decode(path)
|
||||||
|
rescue
|
||||||
|
ArgumentError ->
|
||||||
|
raise InvalidPathError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp allowed?(_only_rules, []), do: false
|
||||||
|
defp allowed?({[], []}, _list), do: true
|
||||||
|
|
||||||
|
defp allowed?({full, prefix}, [h | _]) do
|
||||||
|
h in full or (prefix != [] and match?({0, _}, :binary.match(h, prefix)))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_put_content_type(conn, false, _, _), do: conn
|
||||||
|
|
||||||
|
defp maybe_put_content_type(conn, _, types, filename) do
|
||||||
|
content_type = Map.get(types, filename) || MIME.from_path(filename)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_header("content-type", content_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp serve_static({content_encoding, file_info, path}, conn, segments, range, options) do
|
||||||
|
%{
|
||||||
|
qs_cache: qs_cache,
|
||||||
|
et_cache: et_cache,
|
||||||
|
et_generation: et_generation,
|
||||||
|
headers: headers,
|
||||||
|
content_types: types,
|
||||||
|
set_content_type: set_content_type
|
||||||
|
} = options
|
||||||
|
|
||||||
|
case put_cache_header(conn, qs_cache, et_cache, et_generation, file_info, path) do
|
||||||
|
{:stale, conn} ->
|
||||||
|
filename = List.last(segments)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> maybe_put_content_type(set_content_type, types, filename)
|
||||||
|
|> put_resp_header("accept-ranges", "bytes")
|
||||||
|
|> maybe_add_encoding(content_encoding)
|
||||||
|
|> merge_headers(headers)
|
||||||
|
|> serve_range(file_info, path, range, options)
|
||||||
|
|
||||||
|
{:fresh, conn} ->
|
||||||
|
conn
|
||||||
|
|> maybe_add_vary(options)
|
||||||
|
|> send_resp(304, "")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp serve_static(:error, conn, _segments, _range, _options) do
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
|
||||||
|
defp serve_range(conn, file_info, path, [range], options) do
|
||||||
|
file_info(size: file_size) = file_info
|
||||||
|
|
||||||
|
with %{"bytes" => bytes} <- Plug.Conn.Utils.params(range),
|
||||||
|
{range_start, range_end} <- start_and_end(bytes, file_size) do
|
||||||
|
send_range(conn, path, range_start, range_end, file_size, options)
|
||||||
|
else
|
||||||
|
_ -> send_entire_file(conn, path, options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp serve_range(conn, _file_info, path, _range, options) do
|
||||||
|
send_entire_file(conn, path, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp start_and_end("-" <> rest, file_size) do
|
||||||
|
case Integer.parse(rest) do
|
||||||
|
{last, ""} when last > 0 and last <= file_size -> {file_size - last, file_size - 1}
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp start_and_end(range, file_size) do
|
||||||
|
case Integer.parse(range) do
|
||||||
|
{first, "-"} when first >= 0 ->
|
||||||
|
{first, file_size - 1}
|
||||||
|
|
||||||
|
{first, "-" <> rest} when first >= 0 ->
|
||||||
|
case Integer.parse(rest) do
|
||||||
|
{last, ""} when last >= first -> {first, min(last, file_size - 1)}
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_range(conn, path, 0, range_end, file_size, options) when range_end == file_size - 1 do
|
||||||
|
send_entire_file(conn, path, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_range(conn, path, range_start, range_end, file_size, _options) do
|
||||||
|
length = range_end - range_start + 1
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_resp_header("content-range", "bytes #{range_start}-#{range_end}/#{file_size}")
|
||||||
|
|> send_file(206, path, range_start, length)
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_entire_file(conn, path, options) do
|
||||||
|
conn
|
||||||
|
|> maybe_add_vary(options)
|
||||||
|
|> send_file(200, path)
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_add_encoding(conn, nil), do: conn
|
||||||
|
defp maybe_add_encoding(conn, ce), do: put_resp_header(conn, "content-encoding", ce)
|
||||||
|
|
||||||
|
defp maybe_add_vary(conn, %{encodings: encodings}) do
|
||||||
|
# If we serve gzip or brotli at any moment, we need to set the proper vary
|
||||||
|
# header regardless of whether we are serving gzip content right now.
|
||||||
|
# See: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
|
||||||
|
if encodings != [] do
|
||||||
|
update_in(conn.resp_headers, &[{"vary", "Accept-Encoding"} | &1])
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_cache_header(
|
||||||
|
%Conn{query_string: "vsn=" <> _} = conn,
|
||||||
|
qs_cache,
|
||||||
|
_et_cache,
|
||||||
|
_et_generation,
|
||||||
|
_file_info,
|
||||||
|
_path
|
||||||
|
)
|
||||||
|
when is_binary(qs_cache) do
|
||||||
|
{:stale, put_resp_header(conn, "cache-control", qs_cache)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_cache_header(conn, _qs_cache, et_cache, et_generation, file_info, path)
|
||||||
|
when is_binary(et_cache) do
|
||||||
|
etag = etag_for_path(file_info, et_generation, path)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_resp_header("cache-control", et_cache)
|
||||||
|
|> put_resp_header("etag", etag)
|
||||||
|
|
||||||
|
if etag in get_req_header(conn, "if-none-match") do
|
||||||
|
{:fresh, conn}
|
||||||
|
else
|
||||||
|
{:stale, conn}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_cache_header(conn, _, _, _, _, _) do
|
||||||
|
{:stale, conn}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp etag_for_path(file_info, et_generation, path) do
|
||||||
|
case et_generation do
|
||||||
|
{module, function, args} ->
|
||||||
|
apply(module, function, [path | args])
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
file_info(size: size, mtime: mtime) = file_info
|
||||||
|
<<?", {size, mtime} |> :erlang.phash2() |> Integer.to_string(16)::binary, ?">>
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp file_encoding(conn, path, [_range], _encodings) do
|
||||||
|
# We do not support compression for range queries.
|
||||||
|
file_encoding(conn, path, nil, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp file_encoding(conn, path, _range, encodings) do
|
||||||
|
encoded =
|
||||||
|
Enum.find_value(encodings, fn {encoding, ext} ->
|
||||||
|
if file_info = accept_encoding?(conn, encoding) && regular_file_info(path <> ext) do
|
||||||
|
{encoding, file_info, path <> ext}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
not is_nil(encoded) ->
|
||||||
|
encoded
|
||||||
|
|
||||||
|
file_info = regular_file_info(path) ->
|
||||||
|
{nil, file_info, path}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp regular_file_info(path) do
|
||||||
|
case :prim_file.read_file_info(path) do
|
||||||
|
{:ok, file_info(type: :regular) = file_info} ->
|
||||||
|
file_info
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp accept_encoding?(conn, encoding) do
|
||||||
|
encoding? = &String.contains?(&1, [encoding, "*"])
|
||||||
|
|
||||||
|
Enum.any?(get_req_header(conn, "accept-encoding"), fn accept ->
|
||||||
|
accept |> Plug.Conn.Utils.list() |> Enum.any?(encoding?)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_add(list, key, value, true), do: list ++ [{key, value}]
|
||||||
|
defp maybe_add(list, _key, _value, false), do: list
|
||||||
|
|
||||||
|
defp path({module, function, arguments}, segments)
|
||||||
|
when is_atom(module) and is_atom(function) and is_list(arguments),
|
||||||
|
do: Enum.join([apply(module, function, arguments) | segments], "/")
|
||||||
|
|
||||||
|
defp path({app, from}, segments) when is_atom(app) and is_binary(from),
|
||||||
|
do: Enum.join([Application.app_dir(app), from | segments], "/")
|
||||||
|
|
||||||
|
defp path(from, segments),
|
||||||
|
do: Enum.join([from | segments], "/")
|
||||||
|
|
||||||
|
defp subset([h | expected], [h | actual]), do: subset(expected, actual)
|
||||||
|
defp subset([], actual), do: actual
|
||||||
|
defp subset(_, _), do: []
|
||||||
|
|
||||||
|
defp invalid_path?(list) do
|
||||||
|
invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"]))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp invalid_path?([h | _], _match) when h in [".", "..", ""], do: true
|
||||||
|
defp invalid_path?([h | t], match), do: String.contains?(h, match) or invalid_path?(t)
|
||||||
|
defp invalid_path?([], _match), do: false
|
||||||
|
|
||||||
|
defp merge_headers(conn, {module, function, args}) do
|
||||||
|
merge_headers(conn, apply(module, function, [conn | args]))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp merge_headers(conn, headers) do
|
||||||
|
merge_resp_headers(conn, headers)
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias Pleroma.Web.MediaProxy
|
alias Pleroma.Web.MediaProxy
|
||||||
|
alias Pleroma.Web.Plugs.Utils
|
||||||
|
|
||||||
@behaviour Plug
|
@behaviour Plug
|
||||||
# no slashes
|
# no slashes
|
||||||
|
@ -28,10 +29,21 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
|
||||||
|> Keyword.put(:at, "/__unconfigured_media_plug")
|
|> Keyword.put(:at, "/__unconfigured_media_plug")
|
||||||
|> Plug.Static.init()
|
|> Plug.Static.init()
|
||||||
|
|
||||||
%{static_plug_opts: static_plug_opts}
|
config = Pleroma.Config.get(Pleroma.Upload)
|
||||||
|
allowed_mime_types = Keyword.fetch!(config, :allowed_mime_types)
|
||||||
|
uploader = Keyword.fetch!(config, :uploader)
|
||||||
|
|
||||||
|
%{
|
||||||
|
static_plug_opts: static_plug_opts,
|
||||||
|
allowed_mime_types: allowed_mime_types,
|
||||||
|
uploader: uploader
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
|
def call(
|
||||||
|
%{request_path: <<"/", @path, "/", file::binary>>} = conn,
|
||||||
|
%{uploader: uploader} = opts
|
||||||
|
) do
|
||||||
conn =
|
conn =
|
||||||
case fetch_query_params(conn) do
|
case fetch_query_params(conn) do
|
||||||
%{query_params: %{"name" => name}} = conn ->
|
%{query_params: %{"name" => name}} = conn ->
|
||||||
|
@ -44,10 +56,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
|
||||||
end
|
end
|
||||||
|> merge_resp_headers([{"content-security-policy", "sandbox"}])
|
|> merge_resp_headers([{"content-security-policy", "sandbox"}])
|
||||||
|
|
||||||
config = Pleroma.Config.get(Pleroma.Upload)
|
with {:ok, get_method} <- uploader.get_file(file),
|
||||||
|
|
||||||
with uploader <- Keyword.fetch!(config, :uploader),
|
|
||||||
{:ok, get_method} <- uploader.get_file(file),
|
|
||||||
false <- media_is_banned(conn, get_method) do
|
false <- media_is_banned(conn, get_method) do
|
||||||
get_media(conn, get_method, opts)
|
get_media(conn, get_method, opts)
|
||||||
else
|
else
|
||||||
|
@ -68,13 +77,23 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
|
||||||
|
|
||||||
defp media_is_banned(_, _), do: false
|
defp media_is_banned(_, _), do: false
|
||||||
|
|
||||||
|
defp set_content_type(conn, opts, filepath) do
|
||||||
|
real_mime = MIME.from_path(filepath)
|
||||||
|
clean_mime = Utils.get_safe_mime_type(opts, real_mime)
|
||||||
|
put_resp_header(conn, "content-type", clean_mime)
|
||||||
|
end
|
||||||
|
|
||||||
defp get_media(conn, {:static_dir, directory}, opts) do
|
defp get_media(conn, {:static_dir, directory}, opts) do
|
||||||
static_opts =
|
static_opts =
|
||||||
Map.get(opts, :static_plug_opts)
|
Map.get(opts, :static_plug_opts)
|
||||||
|> Map.put(:at, [@path])
|
|> Map.put(:at, [@path])
|
||||||
|> Map.put(:from, directory)
|
|> Map.put(:from, directory)
|
||||||
|
|> Map.put(:set_content_type, false)
|
||||||
|
|
||||||
conn = Plug.Static.call(conn, static_opts)
|
conn =
|
||||||
|
conn
|
||||||
|
|> set_content_type(opts, conn.request_path)
|
||||||
|
|> Pleroma.Web.Plugs.StaticNoCT.call(static_opts)
|
||||||
|
|
||||||
if conn.halted do
|
if conn.halted do
|
||||||
conn
|
conn
|
||||||
|
|
14
lib/pleroma/web/plugs/utils.ex
Normal file
14
lib/pleroma/web/plugs/utils.ex
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# Akkoma: Magically expressive social media
|
||||||
|
# Copyright © 2024 Akkoma Authors <https://akkoma.dev>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.Plugs.Utils do
|
||||||
|
@moduledoc """
|
||||||
|
Some helper functions shared across several plugs
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_safe_mime_type(%{allowed_mime_types: allowed_mime_types} = _opts, mime) do
|
||||||
|
[maintype | _] = String.split(mime, "/", parts: 2)
|
||||||
|
if maintype in allowed_mime_types, do: mime, else: "application/octet-stream"
|
||||||
|
end
|
||||||
|
end
|
|
@ -800,13 +800,9 @@ defmodule Pleroma.Web.Router do
|
||||||
scope "/", Pleroma.Web.ActivityPub do
|
scope "/", Pleroma.Web.ActivityPub do
|
||||||
pipe_through([:activitypub_client])
|
pipe_through([:activitypub_client])
|
||||||
|
|
||||||
get("/api/ap/whoami", ActivityPubController, :whoami)
|
|
||||||
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
|
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
|
||||||
|
|
||||||
get("/users/:nickname/outbox", ActivityPubController, :outbox)
|
get("/users/:nickname/outbox", ActivityPubController, :outbox)
|
||||||
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
|
|
||||||
post("/api/ap/upload_media", ActivityPubController, :upload_media)
|
|
||||||
|
|
||||||
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
|
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,13 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
|
||||||
true <- Visibility.is_public?(activity.object),
|
true <- Visibility.is_public?(activity.object),
|
||||||
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
|
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
|
||||||
%User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
|
%User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
|
||||||
meta = Metadata.build_tags(%{url: activity.data["id"], object: activity.object, user: user})
|
meta =
|
||||||
|
Metadata.build_tags(%{
|
||||||
|
activity_id: notice_id,
|
||||||
|
url: activity.data["id"],
|
||||||
|
object: activity.object,
|
||||||
|
user: user
|
||||||
|
})
|
||||||
|
|
||||||
timeline =
|
timeline =
|
||||||
activity.object.data["context"]
|
activity.object.data["context"]
|
||||||
|
|
|
@ -151,41 +151,40 @@ defmodule Pleroma.Web.Telemetry do
|
||||||
# phoenix.router_dispatch.stop.duration
|
# phoenix.router_dispatch.stop.duration
|
||||||
# pleroma.repo.query.total_time
|
# pleroma.repo.query.total_time
|
||||||
# pleroma.repo.query.queue_time
|
# pleroma.repo.query.queue_time
|
||||||
dist_metrics =
|
dist_metrics = [
|
||||||
[
|
distribution("phoenix.endpoint.stop.duration.fdist",
|
||||||
distribution("phoenix.endpoint.stop.duration.fdist",
|
event_name: [:phoenix, :endpoint, :stop],
|
||||||
event_name: [:phoenix, :endpoint, :stop],
|
measurement: :duration,
|
||||||
measurement: :duration,
|
unit: {:native, :millisecond},
|
||||||
unit: {:native, :millisecond},
|
reporter_options: [
|
||||||
reporter_options: [
|
buckets: simple_buckets
|
||||||
buckets: simple_buckets
|
]
|
||||||
]
|
),
|
||||||
),
|
distribution("pleroma.repo.query.decode_time.fdist",
|
||||||
distribution("pleroma.repo.query.decode_time.fdist",
|
event_name: [:pleroma, :repo, :query],
|
||||||
event_name: [:pleroma, :repo, :query],
|
measurement: :decode_time,
|
||||||
measurement: :decode_time,
|
unit: {:native, :millisecond},
|
||||||
unit: {:native, :millisecond},
|
reporter_options: [
|
||||||
reporter_options: [
|
buckets: simple_buckets_quick
|
||||||
buckets: simple_buckets_quick
|
]
|
||||||
]
|
),
|
||||||
),
|
distribution("pleroma.repo.query.query_time.fdist",
|
||||||
distribution("pleroma.repo.query.query_time.fdist",
|
event_name: [:pleroma, :repo, :query],
|
||||||
event_name: [:pleroma, :repo, :query],
|
measurement: :query_time,
|
||||||
measurement: :query_time,
|
unit: {:native, :millisecond},
|
||||||
unit: {:native, :millisecond},
|
reporter_options: [
|
||||||
reporter_options: [
|
buckets: simple_buckets
|
||||||
buckets: simple_buckets
|
]
|
||||||
]
|
),
|
||||||
),
|
distribution("pleroma.repo.query.idle_time.fdist",
|
||||||
distribution("pleroma.repo.query.idle_time.fdist",
|
event_name: [:pleroma, :repo, :query],
|
||||||
event_name: [:pleroma, :repo, :query],
|
measurement: :idle_time,
|
||||||
measurement: :idle_time,
|
unit: {:native, :millisecond},
|
||||||
unit: {:native, :millisecond},
|
reporter_options: [
|
||||||
reporter_options: [
|
buckets: simple_buckets
|
||||||
buckets: simple_buckets
|
]
|
||||||
]
|
)
|
||||||
)
|
]
|
||||||
]
|
|
||||||
|
|
||||||
vm_metrics =
|
vm_metrics =
|
||||||
sum_counter_pair("vm.memory.total",
|
sum_counter_pair("vm.memory.total",
|
||||||
|
|
|
@ -184,7 +184,13 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
|
||||||
json(conn, emoji)
|
json(conn, emoji)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do
|
def update_notificaton_settings(
|
||||||
|
%{assigns: %{user: user}, body_params: body_params} = conn,
|
||||||
|
params
|
||||||
|
) do
|
||||||
|
# OpenApiSpex 3.x prevents Plug's usual parameter premerging
|
||||||
|
params = Map.merge(params, body_params)
|
||||||
|
|
||||||
with {:ok, _} <- User.update_notification_settings(user, params) do
|
with {:ok, _} <- User.update_notification_settings(user, params) do
|
||||||
json(conn, %{status: "success"})
|
json(conn, %{status: "success"})
|
||||||
end
|
end
|
||||||
|
|
|
@ -65,7 +65,7 @@ defmodule Pleroma.Web.WebFinger do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp gather_aliases(%User{} = user) do
|
defp gather_aliases(%User{} = user) do
|
||||||
[user.ap_id | user.also_known_as]
|
[user.ap_id]
|
||||||
end
|
end
|
||||||
|
|
||||||
def represent_user(user, "JSON") do
|
def represent_user(user, "JSON") do
|
||||||
|
@ -156,11 +156,21 @@ defmodule Pleroma.Web.WebFinger do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||||
def find_lrdd_template(domain) do
|
def find_lrdd_template(domain) do
|
||||||
|
@cachex.fetch!(:host_meta_cache, domain, fn _ ->
|
||||||
|
{:commit, fetch_lrdd_template(domain)}
|
||||||
|
end)
|
||||||
|
rescue
|
||||||
|
e -> {:error, "Cachex error: #{inspect(e)}"}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_lrdd_template(domain) do
|
||||||
# WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1
|
# WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1
|
||||||
meta_url = "https://#{domain}/.well-known/host-meta"
|
meta_url = "https://#{domain}/.well-known/host-meta"
|
||||||
|
|
||||||
with {:ok, %{status: status, body: body}} when status in 200..299 <- HTTP.get(meta_url) do
|
with {:ok, %{status: status, body: body}} when status in 200..299 <-
|
||||||
|
HTTP.Backoff.get(meta_url) do
|
||||||
get_template_from_xml(body)
|
get_template_from_xml(body)
|
||||||
else
|
else
|
||||||
error ->
|
error ->
|
||||||
|
@ -169,7 +179,7 @@ defmodule Pleroma.Web.WebFinger do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_address_from_domain(domain, encoded_account) when is_binary(domain) do
|
defp get_address_from_domain(domain, "acct:" <> _ = encoded_account) when is_binary(domain) do
|
||||||
case find_lrdd_template(domain) do
|
case find_lrdd_template(domain) do
|
||||||
{:ok, template} ->
|
{:ok, template} ->
|
||||||
String.replace(template, "{uri}", encoded_account)
|
String.replace(template, "{uri}", encoded_account)
|
||||||
|
@ -179,6 +189,11 @@ defmodule Pleroma.Web.WebFinger do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_address_from_domain(domain, account) when is_binary(domain) do
|
||||||
|
encoded_account = URI.encode("acct:#{account}")
|
||||||
|
get_address_from_domain(domain, encoded_account)
|
||||||
|
end
|
||||||
|
|
||||||
defp get_address_from_domain(_, _), do: {:error, :webfinger_no_domain}
|
defp get_address_from_domain(_, _), do: {:error, :webfinger_no_domain}
|
||||||
|
|
||||||
@spec finger(String.t()) :: {:ok, map()} | {:error, any()}
|
@spec finger(String.t()) :: {:ok, map()} | {:error, any()}
|
||||||
|
@ -193,11 +208,9 @@ defmodule Pleroma.Web.WebFinger do
|
||||||
URI.parse(account).host
|
URI.parse(account).host
|
||||||
end
|
end
|
||||||
|
|
||||||
encoded_account = URI.encode("acct:#{account}")
|
with address when is_binary(address) <- get_address_from_domain(domain, account),
|
||||||
|
|
||||||
with address when is_binary(address) <- get_address_from_domain(domain, encoded_account),
|
|
||||||
{:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-
|
{:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-
|
||||||
HTTP.get(
|
HTTP.Backoff.get(
|
||||||
address,
|
address,
|
||||||
[{"accept", "application/xrd+xml,application/jrd+json"}]
|
[{"accept", "application/xrd+xml,application/jrd+json"}]
|
||||||
) do
|
) do
|
||||||
|
@ -217,10 +230,28 @@ defmodule Pleroma.Web.WebFinger do
|
||||||
_ ->
|
_ ->
|
||||||
{:error, {:content_type, nil}}
|
{:error, {:content_type, nil}}
|
||||||
end
|
end
|
||||||
|
|> case do
|
||||||
|
{:ok, data} -> validate_webfinger(address, data)
|
||||||
|
error -> error
|
||||||
|
end
|
||||||
else
|
else
|
||||||
error ->
|
error ->
|
||||||
Logger.debug("Couldn't finger #{account}: #{inspect(error)}")
|
Logger.debug("Couldn't finger #{account}: #{inspect(error)}")
|
||||||
error
|
error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp validate_webfinger(request_url, %{"subject" => "acct:" <> acct = subject} = data) do
|
||||||
|
with [_name, acct_host] <- String.split(acct, "@"),
|
||||||
|
{_, url} <- {:address, get_address_from_domain(acct_host, subject)},
|
||||||
|
%URI{host: request_host} <- URI.parse(request_url),
|
||||||
|
%URI{host: acct_host} <- URI.parse(url),
|
||||||
|
{_, true} <- {:hosts_match, acct_host == request_host} do
|
||||||
|
{:ok, data}
|
||||||
|
else
|
||||||
|
_ -> {:error, {:webfinger_invalid, request_url, data}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_webfinger(url, data), do: {:error, {:webfinger_invalid, url, data}}
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,7 +26,7 @@ defmodule Pleroma.Web.XML do
|
||||||
|
|
||||||
def parse_document(text) do
|
def parse_document(text) do
|
||||||
try do
|
try do
|
||||||
doc = SweetXml.parse(text, dtd: :none)
|
doc = SweetXml.parse(text, dtd: :none, quiet: true)
|
||||||
|
|
||||||
{:ok, doc}
|
{:ok, doc}
|
||||||
rescue
|
rescue
|
||||||
|
|
|
@ -21,6 +21,9 @@ defmodule Pleroma.Workers.Cron.PruneDatabaseWorker do
|
||||||
Logger.info("Pruning old undos")
|
Logger.info("Pruning old undos")
|
||||||
ActivityPruner.prune_undos()
|
ActivityPruner.prune_undos()
|
||||||
|
|
||||||
|
Logger.info("Pruning old updates")
|
||||||
|
ActivityPruner.prune_updates()
|
||||||
|
|
||||||
Logger.info("Pruning old removes")
|
Logger.info("Pruning old removes")
|
||||||
ActivityPruner.prune_removes()
|
ActivityPruner.prune_removes()
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,8 @@ defmodule Pleroma.Workers.ReceiverWorker do
|
||||||
else
|
else
|
||||||
{:error, :origin_containment_failed} -> {:discard, :origin_containment_failed}
|
{:error, :origin_containment_failed} -> {:discard, :origin_containment_failed}
|
||||||
{:error, {:reject, reason}} -> {:discard, reason}
|
{:error, {:reject, reason}} -> {:discard, reason}
|
||||||
e -> e
|
{:error, _} = e -> e
|
||||||
|
e -> {:error, e}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,10 +5,42 @@
|
||||||
defmodule Pleroma.Workers.RemoteFetcherWorker do
|
defmodule Pleroma.Workers.RemoteFetcherWorker do
|
||||||
alias Pleroma.Object.Fetcher
|
alias Pleroma.Object.Fetcher
|
||||||
|
|
||||||
use Pleroma.Workers.WorkerHelper, queue: "remote_fetcher"
|
use Pleroma.Workers.WorkerHelper,
|
||||||
|
queue: "remote_fetcher",
|
||||||
|
unique: [period: 300, states: Oban.Job.states(), keys: [:op, :id]]
|
||||||
|
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do
|
def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do
|
||||||
{:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"])
|
case Fetcher.fetch_object_from_id(id, depth: args["depth"]) do
|
||||||
|
{:ok, _object} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, :forbidden} ->
|
||||||
|
{:discard, :forbidden}
|
||||||
|
|
||||||
|
{:error, :not_found} ->
|
||||||
|
{:discard, :not_found}
|
||||||
|
|
||||||
|
{:error, :allowed_depth} ->
|
||||||
|
{:discard, :allowed_depth}
|
||||||
|
|
||||||
|
{:error, :invalid_uri_scheme} ->
|
||||||
|
{:discard, :invalid_uri_scheme}
|
||||||
|
|
||||||
|
{:error, :local_resource} ->
|
||||||
|
{:discard, :local_resource}
|
||||||
|
|
||||||
|
{:reject, _} ->
|
||||||
|
{:discard, :reject}
|
||||||
|
|
||||||
|
{:error, :id_mismatch} ->
|
||||||
|
{:discard, :id_mismatch}
|
||||||
|
|
||||||
|
{:error, _} = e ->
|
||||||
|
e
|
||||||
|
|
||||||
|
e ->
|
||||||
|
{:error, e}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,12 +25,16 @@ defmodule Pleroma.Workers.WorkerHelper do
|
||||||
defmacro __using__(opts) do
|
defmacro __using__(opts) do
|
||||||
caller_module = __CALLER__.module
|
caller_module = __CALLER__.module
|
||||||
queue = Keyword.fetch!(opts, :queue)
|
queue = Keyword.fetch!(opts, :queue)
|
||||||
|
# by default just stop unintended duplicates - this can and should be overridden
|
||||||
|
# if you want to have a more complex uniqueness constraint
|
||||||
|
uniqueness = Keyword.get(opts, :unique, period: 1)
|
||||||
|
|
||||||
quote do
|
quote do
|
||||||
# Note: `max_attempts` is intended to be overridden in `new/2` call
|
# Note: `max_attempts` is intended to be overridden in `new/2` call
|
||||||
use Oban.Worker,
|
use Oban.Worker,
|
||||||
queue: unquote(queue),
|
queue: unquote(queue),
|
||||||
max_attempts: 1
|
max_attempts: 1,
|
||||||
|
unique: unquote(uniqueness)
|
||||||
|
|
||||||
alias Oban.Job
|
alias Oban.Job
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue