forked from AkkomaGang/akkoma
Compare commits
194 commits
Author | SHA1 | Date | |
---|---|---|---|
74182abb5b | |||
0a9cf8fa8b | |||
066d5b48ed | |||
4a05b2d643 | |||
93200a8073 | |||
41a4ed1db5 | |||
8e789c6236 | |||
184c62359f | |||
829af03042 | |||
842414b927 | |||
f176294d6d | |||
b1c0b9e01a | |||
fc2c740008 | |||
9da2cb881e | |||
522a168af6 | |||
59ea358e52 | |||
d62808e4b6 | |||
7ccc560e4d | |||
a47b02cb69 | |||
6222936673 | |||
d65cd1b141 | |||
d7dd34f263 | |||
c2f60c9228 | |||
13d650602b | |||
a49f04bb4e | |||
da7998e89e | |||
|
dce07f05d9 | ||
7c23793e55 | |||
8243fc0ef4 | |||
11ad4711eb | |||
d8e40173bf | |||
9cc5fe9a5f | |||
355263858c | |||
a7b4e4bfd9 | |||
51642a90c5 | |||
bc79bd0edf | |||
ee61ce61a7 | |||
8a0d130976 | |||
898b98e5dd | |||
ea2de1f28a | |||
2a4587f201 | |||
3460f41776 | |||
cc5c1bb10c | |||
70fe99d196 | |||
366065c0f6 | |||
b5fa8c6d09 | |||
d68a5f6c56 | |||
4231345f4e | |||
96fe080e6e | |||
7151ef4718 | |||
c8e0f7848b | |||
98c7e9534e | |||
1c2eb4d799 | |||
7998a00346 | |||
4c41f8c286 | |||
f0a99b4595 | |||
a1c841a122 | |||
1b09b9fc22 | |||
46148c0825 | |||
4701aa2a38 | |||
fb3de8045a | |||
b2b63ad89f | |||
|
d56165c71e | ||
8fa51700d4 | |||
2ddff7e386 | |||
cd8e6a4235 | |||
ac2327c8fc | |||
92bf93a4f7 | |||
7ad5f8d3c0 | |||
b0387dee14 | |||
caa4fbe326 | |||
09736431e0 | |||
bcf3e101f6 | |||
05bbdbf388 | |||
2c75600532 | |||
0cd4040db6 | |||
0ba5c3649d | |||
8e5defe6ca | |||
138b1aea2f | |||
8b5183cb74 | |||
cbb0d4b0a8 | |||
be2c857845 | |||
9f4d3a936f | |||
f9724b5879 | |||
041dedb86e | |||
280652651c | |||
92544e8f99 | |||
d283ac52c3 | |||
ed4019e7a3 | |||
25d24cc5f6 | |||
ead44c6671 | |||
4859f38624 | |||
0f4a7a185f | |||
|
c17681ae1e | ||
ad92e504d7 | |||
1ffbaa2924 | |||
2e049037de | |||
a2256b3f9e | |||
054396a99e | |||
a846e60d71 | |||
1be18a9f4c | |||
55fc410f80 | |||
ae40ccb8ca | |||
7ed52838f4 | |||
fe23660a2d | |||
177d420ae7 | |||
b0f2ed6eaa | |||
39cef8b8d2 | |||
3ba743d635 | |||
bd02c3d7de | |||
8de373fa24 | |||
d32c4e89bc | |||
7c095a6b70 | |||
e0428a8f14 | |||
e5619c0895 | |||
833d7661d6 | |||
|
d8c7ed70d0 | ||
e8bf4422ff | |||
bcfbfbcff5 | |||
f2e45d4d4b | |||
7615a11a1e | |||
e3c8c4f24f | |||
67cdc38296 | |||
89d209f486 | |||
087ada3b2e | |||
91bedcfa68 | |||
f19d5d1380 | |||
ff5d197341 | |||
7632765b43 | |||
294de939cb | |||
7583eceb38 | |||
|
2624258cfd | ||
|
f646e78b48 | ||
|
d5e9f6be47 | ||
834edfcf96 | |||
79b282dea6 | |||
d1d82782db | |||
|
2b1a252cc7 | ||
f048e0cf1b | |||
416aebb76a | |||
932810c35e | |||
4c7ef1e027 | |||
0f9c9aac38 | |||
c0a99df06a | |||
0cb4c35ee4 | |||
c9b3fcc1d3 | |||
11c5838947 | |||
6ed5be61ff | |||
180dc8b472 | |||
d330c57cda | |||
bd64d07082 | |||
9d2c558f64 | |||
ac25b051ae | |||
c5a44a59db | |||
58d5d9d7bf | |||
b6e8fde4dd | |||
bee10eab5e | |||
13215f5f06 | |||
430b376ded | |||
ccf1007883 | |||
6da783b84d | |||
8f322456a0 | |||
9c876cea21 | |||
9728e2f8f7 | |||
b0f7da9ce0 | |||
fc99c694e6 | |||
fbb13fde76 | |||
cbd236aeb5 | |||
71d3bbd7be | |||
98a3dab10a | |||
88a8086ad3 | |||
40da4e88ea | |||
a2e397a79d | |||
e4332d06b5 | |||
|
661b7fedb6 | ||
|
8b5aca9619 | ||
f101886709 | |||
09fa7227f6 | |||
d5b0720596 | |||
940792f8ba | |||
|
90adb3cff5 | ||
|
f6422cb370 | ||
a3101a435b | |||
d488cf476e | |||
ca182a0ae7 | |||
495a1a71e8 | |||
cf19d4901f | |||
07539f7825 | |||
0ab2f2ab45 | |||
95ed4931f8 | |||
7cd3954152 | |||
80a4e30be7 | |||
4ff5293093 | |||
97037c0b53 |
155 changed files with 3447 additions and 1605 deletions
|
@ -1,6 +1,5 @@
|
|||
[
|
||||
import_deps: [:ecto, :ecto_sql, :phoenix],
|
||||
subdirectories: ["priv/*/migrations"],
|
||||
import_deps: [:mneme, :ecto, :ecto_sql, :phoenix],
|
||||
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||
inputs: [
|
||||
"mix.exs",
|
||||
|
|
|
@ -6,9 +6,12 @@ depends_on:
|
|||
|
||||
variables:
|
||||
- &scw-secrets
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
SCW_ACCESS_KEY:
|
||||
from_secret: SCW_ACCESS_KEY
|
||||
SCW_SECRET_KEY:
|
||||
from_secret: SCW_SECRET_KEY
|
||||
SCW_DEFAULT_ORGANIZATION_ID:
|
||||
from_secret: SCW_DEFAULT_ORGANIZATION_ID
|
||||
- &setup-hex "mix local.hex --force && mix local.rebar --force"
|
||||
- &on-release
|
||||
when:
|
||||
|
@ -56,7 +59,7 @@ steps:
|
|||
release-debian-bookworm:
|
||||
image: akkoma/releaser
|
||||
<<: *on-release
|
||||
secrets: *scw-secrets
|
||||
environment: *scw-secrets
|
||||
commands:
|
||||
- export SOURCE=akkoma-amd64.zip
|
||||
# AMD64
|
||||
|
@ -85,7 +88,7 @@ steps:
|
|||
release-debian-bullseye:
|
||||
image: akkoma/releaser
|
||||
<<: *on-release
|
||||
secrets: *scw-secrets
|
||||
environment: *scw-secrets
|
||||
commands:
|
||||
- export SOURCE=akkoma-amd64-debian-bullseye.zip
|
||||
# AMD64
|
||||
|
@ -111,7 +114,7 @@ steps:
|
|||
release-musl:
|
||||
image: akkoma/releaser
|
||||
<<: *on-stable
|
||||
secrets: *scw-secrets
|
||||
environment: *scw-secrets
|
||||
commands:
|
||||
- export SOURCE=akkoma-amd64-musl.zip
|
||||
- export DEST=scaleway:akkoma-updates/$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}/akkoma-amd64-musl.zip
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
labels:
|
||||
platform: linux/aarch64
|
||||
platform: linux/arm64
|
||||
|
||||
depends_on:
|
||||
- test
|
||||
|
||||
variables:
|
||||
- &scw-secrets
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
SCW_ACCESS_KEY:
|
||||
from_secret: SCW_ACCESS_KEY
|
||||
SCW_SECRET_KEY:
|
||||
from_secret: SCW_SECRET_KEY
|
||||
SCW_DEFAULT_ORGANIZATION_ID:
|
||||
from_secret: SCW_DEFAULT_ORGANIZATION_ID
|
||||
- &setup-hex "mix local.hex --force && mix local.rebar --force"
|
||||
- &on-release
|
||||
when:
|
||||
|
@ -56,7 +59,7 @@ steps:
|
|||
release-debian-bookworm:
|
||||
image: akkoma/releaser:arm64
|
||||
<<: *on-release
|
||||
secrets: *scw-secrets
|
||||
environment: *scw-secrets
|
||||
commands:
|
||||
- export SOURCE=akkoma-arm64.zip
|
||||
- export DEST=scaleway:akkoma-updates/$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}/akkoma-arm64-ubuntu-jammy.zip
|
||||
|
@ -83,7 +86,7 @@ steps:
|
|||
release-musl:
|
||||
image: akkoma/releaser:arm64
|
||||
<<: *on-stable
|
||||
secrets: *scw-secrets
|
||||
environment: *scw-secrets
|
||||
commands:
|
||||
- export SOURCE=akkoma-arm64-musl.zip
|
||||
- export DEST=scaleway:akkoma-updates/$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}/akkoma-arm64-musl.zip
|
||||
|
|
|
@ -6,10 +6,6 @@ depends_on:
|
|||
- build-amd64
|
||||
|
||||
variables:
|
||||
- &scw-secrets
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
- &setup-hex "mix local.hex --force && mix local.rebar --force"
|
||||
- &on-release
|
||||
when:
|
||||
|
@ -49,12 +45,14 @@ variables:
|
|||
steps:
|
||||
docs:
|
||||
<<: *on-point-release
|
||||
secrets:
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
environment:
|
||||
CI: "true"
|
||||
SCW_ACCESS_KEY:
|
||||
from_secret: SCW_ACCESS_KEY
|
||||
SCW_SECRET_KEY:
|
||||
from_secret: SCW_SECRET_KEY
|
||||
SCW_DEFAULT_ORGANIZATION_ID:
|
||||
from_secret: SCW_DEFAULT_ORGANIZATION_ID
|
||||
image: python:3.10-slim
|
||||
commands:
|
||||
- apt-get update && apt-get install -y rclone wget git zip
|
||||
|
|
|
@ -2,10 +2,6 @@ labels:
|
|||
platform: linux/amd64
|
||||
|
||||
variables:
|
||||
- &scw-secrets
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
- &setup-hex "mix local.hex --force && mix local.rebar --force"
|
||||
- &on-release
|
||||
when:
|
||||
|
@ -44,7 +40,7 @@ variables:
|
|||
|
||||
steps:
|
||||
lint:
|
||||
image: akkoma/ci-base:1.16-otp26
|
||||
image: akkoma/ci-base:1.18-otp27
|
||||
<<: *on-pr-open
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
|
|
|
@ -5,22 +5,20 @@ depends_on:
|
|||
- lint
|
||||
|
||||
matrix:
|
||||
# test the lowest and highest versions
|
||||
ELIXIR_VERSION:
|
||||
- 1.14
|
||||
- 1.15
|
||||
- 1.16
|
||||
- 1.18
|
||||
OTP_VERSION:
|
||||
- 25
|
||||
- 26
|
||||
- 27
|
||||
include:
|
||||
- ELIXIR_VERSION: 1.16
|
||||
OTP_VERSION: 26
|
||||
- ELIXIR_VERSION: 1.14
|
||||
OTP_VERSION: 25
|
||||
- ELIXIR_VERSION: 1.18
|
||||
OTP_VERSION: 27
|
||||
|
||||
variables:
|
||||
- &scw-secrets
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
- &setup-hex "mix local.hex --force && mix local.rebar --force"
|
||||
- &on-release
|
||||
when:
|
||||
|
@ -87,5 +85,5 @@ steps:
|
|||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- mkdir -p test/tmp
|
||||
- mix test --preload-modules --exclude erratic --exclude federated --exclude mocked
|
||||
- mix test --preload-modules --only mocked
|
||||
- mix test --preload-modules --exclude erratic --exclude federated --exclude mocked || mix test --failed
|
||||
- mix test --preload-modules --only mocked || mix test --failed
|
||||
|
|
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -4,10 +4,57 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## UNRELEASED
|
||||
## Unreleased
|
||||
|
||||
## 2025.03
|
||||
|
||||
## Added
|
||||
- Oban (worker) dashboard at `/akkoma/oban`
|
||||
|
||||
## Fixed
|
||||
- fixed some holes in SigningKey verification potentially allowing they key-user mapping to be poisoned
|
||||
- frontend ZIP files can no longer traverse to paths outside their install dir
|
||||
- fixed user updates trying but failing to renew signing key information
|
||||
- fixed signing key refresh on key rotation
|
||||
|
||||
## Changed
|
||||
- Dropped obsolete `ap_enabled` indicator from user table and associated buggy logic
|
||||
- The remote user count in prometheus metrics is now an estimate instead of an exact number
|
||||
since the latter proved unreasonably costly to obtain for a merely nice-to-have statistic
|
||||
- Various other tweaks improving stat query performance and avoiding unecessary work on received AP documents
|
||||
- The HTML content for new posts (both Client-to-Server as well as Server-to-Server communication) will now use a different formatting to represent MFM. See [FEP-c16b](https://codeberg.org/fediverse/fep/src/branch/main/fep/c16b/fep-c16b.md) for more details.
|
||||
- HTTP signatures now test the most likely request-target alias first cutting down on overhead
|
||||
|
||||
## 2025.01.01
|
||||
|
||||
Hotfix: Federation could break if a null value found its way into `should_federate?\1`
|
||||
|
||||
## 2025.01
|
||||
|
||||
## Added
|
||||
- New config option `:instance, :cleanup_attachments_delay`
|
||||
- It is now possible to display custom source URLs in akkoma-fe;
|
||||
the settings are part of the frontend configuration
|
||||
|
||||
## Fixed
|
||||
- Media proxy no longer attempts to proxy embedded images
|
||||
- Fix significant uneccessary overhead of attachment cleanup;
|
||||
it no longer attempts to cleanup attachments of deleted remote posts
|
||||
- Fix “Delete & Redraft” often losing attachments if attachment cleanup was enabled
|
||||
- ObjectAge policy no longer lets unlisted posts slip through
|
||||
- ObjectAge policy no longer leaks belated DMs and follower-only posts
|
||||
- the NodeINfo endpoint now uses the correct content type
|
||||
|
||||
## Changed
|
||||
- Anonymous objects now federate completely without an id
|
||||
adopting a proposed AP spec errata and restoring federation
|
||||
with e.g. IceShrimp.NET and fedify-based implementations
|
||||
|
||||
## 3.13.3
|
||||
|
||||
## BREAKING
|
||||
- Minimum PostgreSQL version is raised to 12
|
||||
- Swagger UI moved from `/akkoma/swaggerui/` to `/pleroma/swaggerui/`
|
||||
|
||||
## Added
|
||||
- Implement [FEP-67ff](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) (federation documentation)
|
||||
|
@ -17,6 +64,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
## Fixed
|
||||
- Meilisearch: order of results returned from our REST API now actually matches how Meilisearch ranks results
|
||||
- Emoji are now federated as anonymous objects, fixing issues with
|
||||
some strict servers e.g. rejecting e.g. remote emoji reactions
|
||||
- AP objects with additional JSON-LD profiles beyond ActivityStreams can now be fetched
|
||||
- Single-selection polls no longer expose the voter_count; MastoAPI demands it be null
|
||||
and this confused some clients leading to vote distributions >100%
|
||||
|
||||
## Changed
|
||||
- Refactored Rich Media to cache the content in the database. Fetching operations that could block status rendering have been eliminated.
|
||||
|
|
|
@ -10,8 +10,10 @@
|
|||
## Supported FEPs
|
||||
|
||||
- [FEP-67ff: FEDERATION](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md)
|
||||
- [FEP-dc88: Formatting Mathematics](https://codeberg.org/fediverse/fep/src/branch/main/fep/dc88/fep-dc88.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)
|
||||
- [FEP-c16b: Formatting MFM functions](https://codeberg.org/fediverse/fep/src/branch/main/fep/c16b/fep-c16b.md)
|
||||
|
||||
## ActivityPub
|
||||
|
||||
|
@ -30,6 +32,10 @@ Akkoma does not perform JSON-LD processing.
|
|||
All AP S2S POST requests to Akkoma instances MUST be signed.
|
||||
Depending on instance configuration the same may be true for GET requests.
|
||||
|
||||
### FEP-c16b: Formatting MFM functions
|
||||
|
||||
The optional extension term `htmlMfm` is currently not used.
|
||||
|
||||
## Nodeinfo
|
||||
|
||||
Akkoma provides many additional entries in its nodeinfo response,
|
||||
|
|
|
@ -54,9 +54,6 @@ If your platform is not supported, or you just want to be able to edit the sourc
|
|||
### Docker
|
||||
Docker installation is supported via [this setup](https://docs.akkoma.dev/stable/installation/docker_en/)
|
||||
|
||||
### Packages
|
||||
Akkoma is packaged for [YunoHost](https://yunohost.org) and can be found and installed from the [YunoHost app catalogue](https://yunohost.org/#/apps).
|
||||
|
||||
### Compilation Troubleshooting
|
||||
If you ever encounter compilation issues during the updating of Akkoma, you can try these commands and see if they fix things:
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
FROM elixir:1.9.4
|
||||
|
||||
RUN apt-get update &&\
|
||||
apt-get install -y libmagic-dev cmake libimage-exiftool-perl ffmpeg &&\
|
||||
mix local.hex --force &&\
|
||||
mix local.rebar --force
|
||||
|
||||
ARG TAG
|
||||
FROM docker.io/hexpm/elixir:${TAG}
|
||||
RUN apk update
|
||||
RUN apk add git gcc g++ musl-dev make cmake file-dev rclone wget zip imagemagick ffmpeg perl-image-exiftool exiftool
|
||||
RUN mkdir /src
|
||||
WORKDIR /src
|
||||
|
|
3
ci/build-all.sh
Executable file
3
ci/build-all.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
./build.sh 1.14-otp25 1.14.3-erlang-25.3.2-alpine-3.18.0
|
||||
./build.sh 1.15-otp25 1.15.8-erlang-25.3.2.18-alpine-3.19.7
|
||||
./build.sh 1.18-otp27 1.18.2-erlang-27.2.4-alpine-3.19.7
|
3
ci/build.sh
Executable file
3
ci/build.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
echo "Building $1 using image tag $2"
|
||||
docker build -t docker.io/akkoma/ci-base:$1 --build-arg=version=$1 --build-arg=TAG=$2 .
|
||||
docker push docker.io/akkoma/ci-base:$1
|
|
@ -1 +0,0 @@
|
|||
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:latest --push .
|
|
@ -166,10 +166,7 @@
|
|||
"application/xrd+xml" => ["xrd+xml"],
|
||||
"application/jrd+json" => ["jrd+json"],
|
||||
"application/activity+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"]
|
||||
"application/ld+json" => ["activity+json"]
|
||||
}
|
||||
|
||||
config :mime, :extensions, %{
|
||||
|
@ -255,6 +252,7 @@
|
|||
external_user_synchronization: true,
|
||||
extended_nickname_format: true,
|
||||
cleanup_attachments: false,
|
||||
cleanup_attachments_delay: 1800,
|
||||
multi_factor_authentication: [
|
||||
totp: [
|
||||
# digits 6 or 8
|
||||
|
@ -302,6 +300,7 @@
|
|||
allow_headings: false,
|
||||
allow_tables: false,
|
||||
allow_fonts: false,
|
||||
allow_math: true,
|
||||
scrub_policy: [
|
||||
Pleroma.HTML.Scrubber.Default,
|
||||
Pleroma.HTML.Transform.MediaProxy
|
||||
|
@ -371,6 +370,7 @@
|
|||
note_replies_output_limit: 5,
|
||||
sign_object_fetches: true,
|
||||
authorized_fetch_mode: false,
|
||||
min_key_refetch_interval: 86_400,
|
||||
max_collection_objects: 50
|
||||
|
||||
config :pleroma, :streamer,
|
||||
|
@ -600,7 +600,7 @@
|
|||
federator_incoming: 5,
|
||||
federator_outgoing: 5,
|
||||
search_indexing: 2,
|
||||
rich_media_backfill: 3
|
||||
rich_media_backfill: 1
|
||||
],
|
||||
timeout: [
|
||||
activity_expiration: :timer.seconds(5),
|
||||
|
|
|
@ -1184,7 +1184,7 @@
|
|||
logoMask: true,
|
||||
minimalScopesMode: false,
|
||||
noAttachmentLinks: false,
|
||||
nsfwCensorImage: "/static/img/nsfw.74818f9.png",
|
||||
nsfwCensorImage: "",
|
||||
postContentType: "text/plain",
|
||||
redirectRootLogin: "/main/friends",
|
||||
redirectRootNoLogin: "/main/all",
|
||||
|
@ -1194,7 +1194,9 @@
|
|||
showInstanceSpecificPanel: false,
|
||||
subjectLineBehavior: "email",
|
||||
theme: "pleroma-dark",
|
||||
webPushNotifications: false
|
||||
webPushNotifications: false,
|
||||
backendCommitUrl: "",
|
||||
frontendCommitUrl: ""
|
||||
}
|
||||
],
|
||||
children: [
|
||||
|
@ -1285,7 +1287,7 @@
|
|||
type: {:string, :image},
|
||||
description:
|
||||
"URL of the image to use for hiding NSFW media attachments in the timeline",
|
||||
suggestions: ["/static/img/nsfw.74818f9.png"]
|
||||
suggestions: [""]
|
||||
},
|
||||
%{
|
||||
key: :postContentType,
|
||||
|
@ -1398,6 +1400,18 @@
|
|||
label: "Stop Gifs",
|
||||
type: :boolean,
|
||||
description: "Whether to pause animated images until they're hovered on"
|
||||
},
|
||||
%{
|
||||
key: :backendCommitUrl,
|
||||
label: "Backend Commit URL",
|
||||
type: :string,
|
||||
description: "URL prefix for backend commit hashes"
|
||||
},
|
||||
%{
|
||||
key: :frontendCommitUrl,
|
||||
label: "Frontend Commit URL",
|
||||
type: :string,
|
||||
description: "URL prefix for frontend commit hashes"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -51,7 +51,8 @@
|
|||
hostname: System.get_env("DB_HOST") || "localhost",
|
||||
pool: Ecto.Adapters.SQL.Sandbox,
|
||||
pool_size: 50,
|
||||
queue_target: 5000
|
||||
queue_target: 5000,
|
||||
log: false
|
||||
|
||||
config :pleroma, :dangerzone, override_repo_pool_size: true
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ To add configuration to your config file, you can copy it from the base config.
|
|||
* `registration_reason_length`: Maximum registration reason length (default: `500`).
|
||||
* `external_user_synchronization`: Enabling following/followers counters synchronization for external users.
|
||||
* `cleanup_attachments`: Remove attachments along with statuses. Does not affect duplicate files and attachments without status. Enabling this will increase load to database when deleting statuses on larger instances.
|
||||
* `cleanup_attachments_delay`: How many seconds to wait after post deletion before attempting to deletion; useful for “delete & redraft” functionality (default: `1800`)
|
||||
* `show_reactions`: Let favourites and emoji reactions be viewed through the API (default: `true`).
|
||||
* `password_reset_token_validity`: The time after which reset tokens aren't accepted anymore, in seconds (default: one day).
|
||||
* `local_bubble`: Array of domains representing instances closely related to yours. Used to populate the `bubble` timeline. e.g `["example.com"]`, (default: `[]`)
|
||||
|
@ -338,7 +339,7 @@ config :pleroma, :frontends,
|
|||
|
||||
* `:primary` - The frontend that will be served at `/`
|
||||
* `:admin` - The frontend that will be served at `/pleroma/admin`
|
||||
* `:swagger` - Config for developers to act as an API reference to be served at `/akkoma/swaggerui/` (trailing slash _needed_). Disabled by default.
|
||||
* `:swagger` - Config for developers to act as an API reference to be served at `/pleroma/swaggerui/` (trailing slash _needed_). Disabled by default.
|
||||
* `:mastodon` - The mastodon-fe configuration. This shouldn't need to be changed. This is served at `/web` when installed.
|
||||
|
||||
### :static\_fe
|
||||
|
|
|
@ -60,4 +60,4 @@ config :pleroma, :frontends,
|
|||
|
||||
Then run the [pleroma.frontend cli task](../../administration/CLI_tasks/frontend) with the name of `swagger-ui` to install the distribution files.
|
||||
|
||||
You will now be able to view documentation at `/akkoma/swaggerui`
|
||||
You will now be able to view documentation at `/pleroma/swaggerui`
|
||||
|
|
|
@ -35,32 +35,24 @@ sudo useradd -r -s /bin/false -m -d /var/lib/akkoma -U akkoma
|
|||
|
||||
### Install Elixir and Erlang
|
||||
|
||||
#### Using `apt`
|
||||
If your distribution packages a recent enough version of Elixir, you can install it directly from the distro repositories and skip to the next section of the guide:
|
||||
|
||||
```shell
|
||||
sudo apt install elixir erlang-dev erlang-nox
|
||||
```
|
||||
|
||||
Otherwise use [asdf](https://github.com/asdf-vm/asdf) to install the latest versions of Elixir and Erlang.
|
||||
#### Using `asdf`
|
||||
If your distribution does not have a recent version of Elxir in their repositories, you can use [asdf](https://asdf-vm.com/) to install a newer version of Elixir and Erlang.
|
||||
|
||||
First, install some dependencies needed to build Elixir and Erlang:
|
||||
```shell
|
||||
sudo apt install curl unzip build-essential autoconf m4 libncurses5-dev libssh-dev unixodbc-dev xsltproc libxml2-utils libncurses-dev
|
||||
```
|
||||
|
||||
Then login to the `akkoma` user and install asdf:
|
||||
```shell
|
||||
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.11.3
|
||||
```
|
||||
Then login to the `akkoma` user.
|
||||
|
||||
Add the following lines to `~/.bashrc`:
|
||||
```shell
|
||||
. "$HOME/.asdf/asdf.sh"
|
||||
# asdf completions
|
||||
. "$HOME/.asdf/completions/asdf.bash"
|
||||
```
|
||||
|
||||
Restart the shell:
|
||||
Install asdf by following steps 1 to 3 on [their website](https://asdf-vm.com/guide/getting-started.html), then restart the shell to load asdf:
|
||||
```shell
|
||||
exec $SHELL
|
||||
```
|
||||
|
@ -69,15 +61,15 @@ Next install Erlang:
|
|||
```shell
|
||||
asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
|
||||
export KERL_CONFIGURE_OPTIONS="--disable-debug --without-javac"
|
||||
asdf install erlang 25.3.2.5
|
||||
asdf global erlang 25.3.2.5
|
||||
asdf install erlang 27.2.4
|
||||
asdf set erlang 27.2.4
|
||||
```
|
||||
|
||||
Now install Elixir:
|
||||
```shell
|
||||
asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
|
||||
asdf install elixir 1.15.4-otp-25
|
||||
asdf global elixir 1.15.4-otp-25
|
||||
asdf plugin add elixir https://github.com/asdf-vm/asdf-elixir.git
|
||||
asdf install elixir 1.18.2-otp-27
|
||||
asdf set elixir 1.18.2-otp-27
|
||||
```
|
||||
|
||||
Confirm that Elixir is installed correctly by checking the version:
|
||||
|
|
|
@ -6,7 +6,9 @@ probably install frontends.
|
|||
These are no longer bundled with the distribution and need an extra
|
||||
command to install.
|
||||
|
||||
For most installations, the following will suffice:
|
||||
You **must** run frontend management tasks as the akkoma user,
|
||||
the same way you downloaded the build or cloned the git repo before.
|
||||
But otherwise, for most installations, the following will suffice:
|
||||
|
||||
=== "OTP"
|
||||
```sh
|
||||
|
@ -28,4 +30,3 @@ For most installations, the following will suffice:
|
|||
```
|
||||
|
||||
For more customised installations, refer to [Frontend Management](../../configuration/frontend_management)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
## Required dependencies
|
||||
|
||||
* PostgreSQL 12+
|
||||
* Elixir 1.14+ (currently tested up to 1.16)
|
||||
* Erlang OTP 25+ (currently tested up to OTP26)
|
||||
* Elixir 1.14.1+ (currently tested up to 1.18)
|
||||
* Erlang OTP 25+ (currently tested up to OTP27)
|
||||
* git
|
||||
* file / libmagic
|
||||
* gcc (clang might also work)
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
# Installing on Yunohost
|
||||
|
||||
[YunoHost](https://yunohost.org) is a server operating system aimed at self-hosting. The YunoHost community maintains a package of Akkoma which allows you to install Akkoma on YunoHost. You can install it via the normal way through the admin web interface, or through the CLI. More information can be found at [the repo of the package](https://github.com/YunoHost-Apps/akkoma_ynh).
|
||||
|
||||
## Questions
|
||||
|
||||
Questions and problems related to the YunoHost parts can be done through the [YunoHost channels](https://yunohost.org/en/help).
|
||||
|
||||
For questions about Akkoma, check out the [Akkoma community channels](../../#community-channels).
|
|
@ -11,11 +11,13 @@
|
|||
#
|
||||
|
||||
daemon="/usr/local/bin/elixir"
|
||||
daemon_flags="--detached -S /usr/local/bin/mix phx.server"
|
||||
daemon_flags="-S /usr/local/bin/mix phx.server"
|
||||
daemon_user="_akkoma"
|
||||
daemon_execdir="/home/_akkoma/akkoma"
|
||||
|
||||
. /etc/rc.d/rc.subr
|
||||
|
||||
rc_bg="YES"
|
||||
rc_reload=NO
|
||||
pexp="phx.server"
|
||||
|
||||
|
@ -24,7 +26,7 @@ rc_check() {
|
|||
}
|
||||
|
||||
rc_start() {
|
||||
${rcexec} "cd akkoma; ${daemon} ${daemon_flags}"
|
||||
rc_exec "${daemon} ${daemon_flags}"
|
||||
}
|
||||
|
||||
rc_stop() {
|
||||
|
|
|
@ -343,10 +343,16 @@ def run(["prune_objects" | args]) do
|
|||
|
||||
%{:num_rows => del_hashtags} =
|
||||
"""
|
||||
DELETE FROM hashtags AS ht
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM hashtags_objects hto
|
||||
WHERE ht.id = hto.hashtag_id)
|
||||
DELETE FROM hashtags
|
||||
USING hashtags AS ht
|
||||
LEFT JOIN hashtags_objects hto
|
||||
ON ht.id = hto.hashtag_id
|
||||
LEFT JOIN user_follows_hashtag ufht
|
||||
ON ht.id = ufht.hashtag_id
|
||||
WHERE
|
||||
hashtags.id = ht.id
|
||||
AND hto.hashtag_id is NULL
|
||||
AND ufht.hashtag_id is NULL
|
||||
"""
|
||||
|> Repo.query!()
|
||||
|
||||
|
|
|
@ -93,6 +93,7 @@ def run(["get-packs" | args]) do
|
|||
)
|
||||
|
||||
files = fetch_and_decode!(files_loc)
|
||||
files_to_unzip = for({_, f} <- files, do: f)
|
||||
|
||||
shell_info(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
|
||||
|
||||
|
@ -103,17 +104,7 @@ def run(["get-packs" | args]) do
|
|||
pack_name
|
||||
])
|
||||
|
||||
files_to_unzip =
|
||||
Enum.map(
|
||||
files,
|
||||
fn {_, f} -> to_charlist(f) end
|
||||
)
|
||||
|
||||
{:ok, _} =
|
||||
:zip.unzip(binary_archive,
|
||||
cwd: pack_path,
|
||||
file_list: files_to_unzip
|
||||
)
|
||||
{:ok, _} = Pleroma.SafeZip.unzip_data(binary_archive, pack_path, files_to_unzip)
|
||||
|
||||
shell_info(IO.ANSI.format(["Writing pack.json for ", :bright, pack_name]))
|
||||
|
||||
|
@ -202,7 +193,7 @@ def run(["gen-pack" | args]) do
|
|||
|
||||
tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")
|
||||
|
||||
{:ok, _} = :zip.unzip(binary_archive, cwd: String.to_charlist(tmp_pack_dir))
|
||||
{:ok, _} = Pleroma.SafeZip.unzip_data(binary_archive, tmp_pack_dir)
|
||||
|
||||
emoji_map = Pleroma.Emoji.Loader.make_shortcode_to_file_map(tmp_pack_dir, exts)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ defmodule Akkoma.Collections.Fetcher do
|
|||
@spec fetch_collection(String.t() | map()) :: {:ok, [Pleroma.Object.t()]} | {:error, any()}
|
||||
def fetch_collection(ap_id) when is_binary(ap_id) do
|
||||
with {:ok, page} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do
|
||||
{:ok, objects_from_collection(page)}
|
||||
partial_as_success(objects_from_collection(page))
|
||||
else
|
||||
e ->
|
||||
Logger.error("Could not fetch collection #{ap_id} - #{inspect(e)}")
|
||||
|
@ -24,9 +24,12 @@ def fetch_collection(ap_id) when is_binary(ap_id) do
|
|||
|
||||
def fetch_collection(%{"type" => type} = page)
|
||||
when type in ["Collection", "OrderedCollection", "CollectionPage", "OrderedCollectionPage"] do
|
||||
{:ok, objects_from_collection(page)}
|
||||
partial_as_success(objects_from_collection(page))
|
||||
end
|
||||
|
||||
defp partial_as_success({:partial, items}), do: {:ok, items}
|
||||
defp partial_as_success(res), do: res
|
||||
|
||||
defp items_in_page(%{"type" => type, "orderedItems" => items})
|
||||
when is_list(items) and type in ["OrderedCollection", "OrderedCollectionPage"],
|
||||
do: items
|
||||
|
@ -53,11 +56,11 @@ defp objects_from_collection(%{"type" => type, "first" => %{"id" => id}})
|
|||
fetch_page_items(id)
|
||||
end
|
||||
|
||||
defp objects_from_collection(_page), do: []
|
||||
defp objects_from_collection(_page), do: {:ok, []}
|
||||
|
||||
defp fetch_page_items(id, items \\ []) do
|
||||
if Enum.count(items) >= Config.get([:activitypub, :max_collection_objects]) do
|
||||
items
|
||||
{:ok, items}
|
||||
else
|
||||
with {:ok, page} <- Fetcher.fetch_and_contain_remote_object_from_id(id) do
|
||||
objects = items_in_page(page)
|
||||
|
@ -65,18 +68,22 @@ defp fetch_page_items(id, items \\ []) do
|
|||
if Enum.count(objects) > 0 do
|
||||
maybe_next_page(page, items ++ objects)
|
||||
else
|
||||
items
|
||||
{:ok, items}
|
||||
end
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
items
|
||||
{:ok, items}
|
||||
|
||||
{:error, :forbidden} ->
|
||||
items
|
||||
{:ok, items}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("Could not fetch page #{id} - #{inspect(error)}")
|
||||
{:error, error}
|
||||
|
||||
case items do
|
||||
[] -> {:error, error}
|
||||
_ -> {:partial, items}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -85,5 +92,5 @@ defp maybe_next_page(%{"next" => id}, items) when is_binary(id) do
|
|||
fetch_page_items(id, items)
|
||||
end
|
||||
|
||||
defp maybe_next_page(_, items), do: items
|
||||
defp maybe_next_page(_, items), do: {:ok, items}
|
||||
end
|
||||
|
|
|
@ -84,8 +84,14 @@ defp default_config(Swoosh.Adapters.SMTP, conf, _) do
|
|||
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,
|
||||
# This allows wildcard ceritifcates to be verified properly.
|
||||
# The :https parameter simply means to use the HTTPS wildcard format
|
||||
# (as opposed to say LDAP). SMTP servers tend to use the same type of
|
||||
# certs as HTTPS ones so this should work for most.
|
||||
customize_hostname_check: [
|
||||
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
|
||||
],
|
||||
# the default of 10 is too restrictive
|
||||
depth: 32
|
||||
]
|
||||
|
|
|
@ -25,6 +25,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
alias Pleroma.Emoji
|
||||
alias Pleroma.Emoji.Pack
|
||||
alias Pleroma.Utils
|
||||
alias Pleroma.SafeZip
|
||||
|
||||
# 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
|
||||
|
@ -95,22 +96,20 @@ def delete(name) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec unpack_zip_emojies(list(tuple())) :: list(map())
|
||||
defp unpack_zip_emojies(zip_files) do
|
||||
Enum.reduce(zip_files, [], fn
|
||||
{_, path, s, _, _, _}, acc when elem(s, 2) == :regular ->
|
||||
with(
|
||||
filename <- Path.basename(path),
|
||||
shortcode <- Path.basename(filename, Path.extname(filename)),
|
||||
false <- Emoji.exist?(shortcode)
|
||||
) do
|
||||
[%{path: path, filename: path, shortcode: shortcode} | acc]
|
||||
else
|
||||
_ -> acc
|
||||
end
|
||||
|
||||
_, acc ->
|
||||
acc
|
||||
@spec map_zip_emojis(list(String.t())) :: list(map())
|
||||
defp map_zip_emojis(zip_files) do
|
||||
Enum.reduce(zip_files, [], fn path, acc ->
|
||||
with(
|
||||
filename <- Path.basename(path),
|
||||
shortcode <- Path.basename(filename, Path.extname(filename)),
|
||||
# note: this only checks the shortcode, if an emoji already exists on the same path, but
|
||||
# with a different shortcode, the existing one will be degraded to an alias of the new
|
||||
false <- Emoji.exist?(shortcode)
|
||||
) do
|
||||
[%{path: path, filename: path, shortcode: shortcode} | acc]
|
||||
else
|
||||
_ -> acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -118,18 +117,15 @@ defp unpack_zip_emojies(zip_files) do
|
|||
{:ok, t()}
|
||||
| {:error, File.posix() | atom()}
|
||||
def add_file(%Pack{} = pack, _, _, %Plug.Upload{content_type: "application/zip"} = file) do
|
||||
with {:ok, zip_files} <- :zip.table(to_charlist(file.path)),
|
||||
[_ | _] = emojies <- unpack_zip_emojies(zip_files),
|
||||
with {:ok, zip_files} <- SafeZip.list_dir_file(file.path),
|
||||
[_ | _] = emojis <- map_zip_emojis(zip_files),
|
||||
{:ok, tmp_dir} <- Utils.tmp_dir("emoji") do
|
||||
try do
|
||||
{:ok, _emoji_files} =
|
||||
:zip.unzip(
|
||||
to_charlist(file.path),
|
||||
[{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}]
|
||||
)
|
||||
SafeZip.unzip_file(file.path, tmp_dir, Enum.map(emojis, & &1[:path]))
|
||||
|
||||
{_, updated_pack} =
|
||||
Enum.map_reduce(emojies, pack, fn item, emoji_pack ->
|
||||
Enum.map_reduce(emojis, pack, fn item, emoji_pack ->
|
||||
emoji_file = %Plug.Upload{
|
||||
filename: item[:filename],
|
||||
path: path_join_safe(tmp_dir, item[:path])
|
||||
|
@ -446,16 +442,9 @@ defp downloadable?(pack) do
|
|||
end
|
||||
|
||||
defp create_archive_and_cache(pack, hash) do
|
||||
files = [
|
||||
~c"pack.json"
|
||||
| Enum.map(pack.files, fn {_, file} ->
|
||||
{:ok, file} = Path.safe_relative(file)
|
||||
to_charlist(file)
|
||||
end)
|
||||
]
|
||||
|
||||
{:ok, {_, result}} =
|
||||
:zip.zip(~c"#{pack.name}.zip", files, [:memory, cwd: to_charlist(pack.path)])
|
||||
pack_file_list = Enum.into(pack.files, [], fn {_, f} -> f end)
|
||||
files = ["pack.json" | pack_file_list]
|
||||
{:ok, {_, result}} = SafeZip.zip("#{pack.name}.zip", files, pack.path, true)
|
||||
|
||||
ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
|
||||
overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files))
|
||||
|
@ -626,11 +615,10 @@ defp copy_as(remote_pack, local_name) do
|
|||
|
||||
defp unzip(archive, pack_info, remote_pack, local_pack) do
|
||||
with :ok <- File.mkdir_p!(local_pack.path) do
|
||||
files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end)
|
||||
files = Enum.map(remote_pack["files"], fn {_, path} -> path end)
|
||||
# Fallback cannot contain a pack.json file
|
||||
files = if pack_info[:fallback], do: files, else: [~c"pack.json" | files]
|
||||
|
||||
:zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files)
|
||||
files = if pack_info[:fallback], do: files, else: ["pack.json" | files]
|
||||
SafeZip.unzip_data(archive, local_pack.path, files)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -693,13 +681,14 @@ defp update_sha_and_save_metadata(pack, data) do
|
|||
end
|
||||
|
||||
defp validate_has_all_files(pack, zip) do
|
||||
with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do
|
||||
# Check if all files from the pack.json are in the archive
|
||||
pack.files
|
||||
|> Enum.all?(fn {_, from_manifest} ->
|
||||
List.keyfind(f_list, to_charlist(from_manifest), 0)
|
||||
# Check if all files from the pack.json are in the archive
|
||||
eset =
|
||||
Enum.reduce(pack.files, MapSet.new(), fn
|
||||
{_, file}, s -> MapSet.put(s, to_charlist(file))
|
||||
end)
|
||||
|> if(do: :ok, else: {:error, :incomplete})
|
||||
end
|
||||
|
||||
if SafeZip.contains_all_data?(zip, eset),
|
||||
do: :ok,
|
||||
else: {:error, :incomplete}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -70,21 +70,12 @@ defp download_or_unzip(_frontend_info, temp_dir, file) do
|
|||
end
|
||||
|
||||
def unzip(zip, dest) do
|
||||
with {:ok, unzipped} <- :zip.unzip(zip, [:memory]) do
|
||||
File.rm_rf!(dest)
|
||||
File.mkdir_p!(dest)
|
||||
File.rm_rf!(dest)
|
||||
File.mkdir_p!(dest)
|
||||
|
||||
Enum.each(unzipped, fn {filename, data} ->
|
||||
path = filename
|
||||
|
||||
new_file_path = Path.join(dest, path)
|
||||
|
||||
new_file_path
|
||||
|> Path.dirname()
|
||||
|> File.mkdir_p!()
|
||||
|
||||
File.write!(new_file_path, data)
|
||||
end)
|
||||
case Pleroma.SafeZip.unzip_data(zip, dest) do
|
||||
{:ok, _} -> :ok
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -158,6 +158,14 @@ def needs_update(%Instance{metadata_updated_at: metadata_updated_at}) do
|
|||
NaiveDateTime.diff(now, metadata_updated_at) > 86_400
|
||||
end
|
||||
|
||||
def needs_update(%URI{host: host}) do
|
||||
with %Instance{} = instance <- Repo.get_by(Instance, %{host: host}) do
|
||||
needs_update(instance)
|
||||
else
|
||||
_ -> true
|
||||
end
|
||||
end
|
||||
|
||||
def local do
|
||||
%Instance{
|
||||
host: Pleroma.Web.Endpoint.host(),
|
||||
|
@ -180,7 +188,7 @@ def update_metadata(%URI{host: host} = uri) do
|
|||
defp do_update_metadata(%URI{host: host} = uri, existing_record) do
|
||||
if existing_record do
|
||||
if needs_update(existing_record) do
|
||||
Logger.info("Updating metadata for #{host}")
|
||||
Logger.debug("Updating metadata for #{host}")
|
||||
favicon = scrape_favicon(uri)
|
||||
nodeinfo = scrape_nodeinfo(uri)
|
||||
|
||||
|
@ -199,7 +207,7 @@ defp do_update_metadata(%URI{host: host} = uri, existing_record) do
|
|||
favicon = scrape_favicon(uri)
|
||||
nodeinfo = scrape_nodeinfo(uri)
|
||||
|
||||
Logger.info("Creating metadata for #{host}")
|
||||
Logger.debug("Creating metadata for #{host}")
|
||||
|
||||
%Instance{}
|
||||
|> changeset(%{
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Keys do
|
||||
# Native generation of RSA keys is only available since OTP 20+ and in default build conditions
|
||||
# We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
|
||||
try do
|
||||
_ = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
|
||||
def generate_rsa_pem do
|
||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
|
||||
{:ok, pem}
|
||||
end
|
||||
rescue
|
||||
_ ->
|
||||
def generate_rsa_pem do
|
||||
port = Port.open({:spawn, "openssl genrsa"}, [:binary])
|
||||
|
||||
{:ok, pem} =
|
||||
receive do
|
||||
{^port, {:data, pem}} -> {:ok, pem}
|
||||
end
|
||||
|
||||
Port.close(port)
|
||||
|
||||
if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
|
||||
{:ok, pem}
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def keys_from_pem(pem) do
|
||||
with [private_key_code] <- :public_key.pem_decode(pem),
|
||||
private_key <- :public_key.pem_entry_decode(private_key_code),
|
||||
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do
|
||||
{:ok, private_key, {:RSAPublicKey, modulus, exponent}}
|
||||
else
|
||||
error -> {:error, error}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,7 +9,6 @@ defmodule Pleroma.Object do
|
|||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Hashtag
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Fetcher
|
||||
|
@ -216,6 +215,11 @@ def get_cached_by_ap_id(ap_id) do
|
|||
end
|
||||
end
|
||||
|
||||
# Intentionally accepts non-Object arguments!
|
||||
@spec tombstone_object?(term()) :: boolean()
|
||||
def tombstone_object?(%Object{data: %{"type" => "Tombstone"}}), do: true
|
||||
def tombstone_object?(_), do: false
|
||||
|
||||
def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
|
||||
%ObjectTombstone{
|
||||
id: id,
|
||||
|
@ -241,23 +245,11 @@ def delete(%Object{data: %{"id" => id}} = object) do
|
|||
with {:ok, _obj} = swap_object_with_tombstone(object),
|
||||
deleted_activity = Activity.delete_all_by_object_ap_id(id),
|
||||
{:ok, _} <- invalid_object_cache(object) do
|
||||
cleanup_attachments(
|
||||
Config.get([:instance, :cleanup_attachments]),
|
||||
%{object: object}
|
||||
)
|
||||
|
||||
AttachmentsCleanupWorker.enqueue_if_needed(object.data)
|
||||
{:ok, object, deleted_activity}
|
||||
end
|
||||
end
|
||||
|
||||
@spec cleanup_attachments(boolean(), %{required(:object) => map()}) ::
|
||||
{:ok, Oban.Job.t() | nil}
|
||||
def cleanup_attachments(true, %{object: _} = params) do
|
||||
AttachmentsCleanupWorker.enqueue("cleanup_attachments", params)
|
||||
end
|
||||
|
||||
def cleanup_attachments(_, _), do: {:ok, nil}
|
||||
|
||||
def prune(%Object{data: %{"id" => _id}} = object) do
|
||||
with {:ok, object} <- Repo.delete(object),
|
||||
{:ok, _} <- invalid_object_cache(object) do
|
||||
|
|
|
@ -12,8 +12,6 @@ defmodule Pleroma.Object.Containment do
|
|||
spoofing, therefore removal of object containment functions is NOT recommended.
|
||||
"""
|
||||
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
def get_actor(%{"actor" => actor}) when is_binary(actor) do
|
||||
actor
|
||||
end
|
||||
|
@ -50,16 +48,39 @@ def get_object(_) do
|
|||
defp compare_uris(%URI{host: host} = _id_uri, %URI{host: host} = _other_uri), do: :ok
|
||||
defp compare_uris(_id_uri, _other_uri), do: :error
|
||||
|
||||
defp compare_uris_exact(uri, uri), do: :ok
|
||||
defp uri_strip_slash(%URI{path: path} = uri) when is_binary(path),
|
||||
do: %{uri | path: String.replace_suffix(path, "/", "")}
|
||||
|
||||
defp compare_uris_exact(%URI{} = id, %URI{} = other),
|
||||
do: compare_uris_exact(URI.to_string(id), URI.to_string(other))
|
||||
defp uri_strip_slash(uri), do: uri
|
||||
|
||||
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
|
||||
# domain names are case-insensitive per spec (other parts of URIs aren’t necessarily)
|
||||
defp uri_normalise_host(%URI{host: host} = uri) when is_binary(host),
|
||||
do: %{uri | host: String.downcase(host, :ascii)}
|
||||
|
||||
defp uri_normalise_host(uri), do: uri
|
||||
|
||||
defp compare_uri_identities(uri, uri), do: :ok
|
||||
|
||||
defp compare_uri_identities(id_uri, other_uri) when is_binary(id_uri) and is_binary(other_uri),
|
||||
do: compare_uri_identities(URI.parse(id_uri), URI.parse(other_uri))
|
||||
|
||||
defp compare_uri_identities(%URI{} = id, %URI{} = other) do
|
||||
normid =
|
||||
%{id | fragment: nil}
|
||||
|> uri_strip_slash()
|
||||
|> uri_normalise_host()
|
||||
|
||||
normother =
|
||||
%{other | fragment: nil}
|
||||
|> uri_strip_slash()
|
||||
|> uri_normalise_host()
|
||||
|
||||
# Conversion back to binary avoids issues from non-normalised deprecated authority field
|
||||
if URI.to_string(normid) == URI.to_string(normother) do
|
||||
:ok
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -93,21 +114,13 @@ def contain_origin(id, %{"attributedTo" => actor} = params),
|
|||
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)
|
||||
Check whether the fetch URL (after redirects) is the
|
||||
same location the canonical ActivityPub id points to.
|
||||
|
||||
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
|
||||
def contain_id_to_fetch(url, %{"id" => id}) when is_binary(id) do
|
||||
compare_uri_identities(url, id)
|
||||
end
|
||||
|
||||
def contain_id_to_fetch(_url, _data), do: :error
|
||||
|
@ -144,4 +157,16 @@ def same_origin(id1, id2) do
|
|||
|
||||
compare_uris(uri1, uri2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks whether a key_id - owner_id pair are acceptable.
|
||||
|
||||
While in theory keys and actors on different domain could be verified
|
||||
by fetching both and checking the links on both ends (if different at all),
|
||||
this requires extra fetches and there are no known implementations with split
|
||||
actor and key domains, thus atm this simply requires same domain.
|
||||
"""
|
||||
def contain_key_user(key_id, user_id) do
|
||||
same_origin(key_id, user_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -116,7 +116,7 @@ defp reinject_object(%Object{} = object, new_data) do
|
|||
@doc "Assumes object already is in our database and refetches from remote to update (e.g. for polls)"
|
||||
def refetch_object(%Object{data: %{"id" => id}} = object) do
|
||||
with {:local, false} <- {:local, Object.local?(object)},
|
||||
{:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
|
||||
{:ok, new_data} <- fetch_and_contain_remote_object_from_id(id, true),
|
||||
{:id, true} <- {:id, new_data["id"] == id},
|
||||
{:ok, object} <- reinject_object(object, new_data) do
|
||||
{:ok, object}
|
||||
|
@ -253,14 +253,75 @@ defp maybe_date_fetch(headers, date) do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Fetches arbitrary remote object and performs basic safety and authenticity checks"
|
||||
def fetch_and_contain_remote_object_from_id(id)
|
||||
@doc """
|
||||
Fetches arbitrary remote object and performs basic safety and authenticity checks.
|
||||
When the fetch URL is known to already be a canonical AP id, checks are stricter.
|
||||
"""
|
||||
def fetch_and_contain_remote_object_from_id(id, is_ap_id \\ false)
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(%{"id" => id}),
|
||||
do: fetch_and_contain_remote_object_from_id(id)
|
||||
def fetch_and_contain_remote_object_from_id(%{"id" => id}, is_ap_id),
|
||||
do: fetch_and_contain_remote_object_from_id(id, is_ap_id)
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
||||
Logger.debug("Fetching object #{id} via AP")
|
||||
def fetch_and_contain_remote_object_from_id(id, is_ap_id) when is_binary(id) do
|
||||
Logger.debug("Fetching object #{id} via AP [ap_id=#{is_ap_id}]")
|
||||
|
||||
fetch_and_contain_remote_ap_doc(
|
||||
id,
|
||||
is_ap_id,
|
||||
fn final_uri, data -> {Containment.contain_id_to_fetch(final_uri, data), data["id"]} end
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(_id, _is_ap_id),
|
||||
do: {:error, :invalid_id}
|
||||
|
||||
def fetch_and_contain_remote_key(id) do
|
||||
Logger.debug("Fetching remote actor key #{id}")
|
||||
|
||||
fetch_and_contain_remote_ap_doc(
|
||||
id,
|
||||
# key IDs are alwas AP IDs which should resolve directly and exactly
|
||||
true,
|
||||
fn
|
||||
final_uri, %{"id" => user_id, "publicKey" => %{"id" => key_id}} ->
|
||||
# For non-fragment keys as used e.g. by GTS, the "id" won't match the fetch URI,
|
||||
# but the key ID will. Thus do NOT strict check the top-lelve id, but byte-exact
|
||||
# check the key ID (since for later lookups we need byte-exact matches).
|
||||
# This relies on fetching already enforcing a domain match for toplevel id and host
|
||||
with :ok <- Containment.contain_key_user(key_id, user_id),
|
||||
true <- key_id == final_uri do
|
||||
{:ok, key_id}
|
||||
else
|
||||
_ -> {:error, key_id}
|
||||
end
|
||||
|
||||
final_uri,
|
||||
%{"type" => "CryptographicKey", "id" => key_id, "owner" => user_id, "publicKeyPem" => _} ->
|
||||
# XXX: refactor into separate function isntead of duplicating
|
||||
with :ok <- Containment.contain_key_user(key_id, user_id),
|
||||
true <- key_id == final_uri do
|
||||
{:ok, key_id}
|
||||
else
|
||||
_ -> {:error, nil}
|
||||
end
|
||||
|
||||
_, _ ->
|
||||
{:error, nil}
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
# Fetches an AP document and performing variable security checks on it.
|
||||
#
|
||||
# Note that the received documents "id" matching the final host domain
|
||||
# is always enforced before the custom ID check runs.
|
||||
@spec fetch_and_contain_remote_ap_doc(
|
||||
String.t(),
|
||||
boolean(),
|
||||
(String.t(), Map.t() -> {:ok | :error, String.t() | term()})
|
||||
) :: {:ok, Map.t()} | {:reject, term()} | {:error, term()}
|
||||
defp fetch_and_contain_remote_ap_doc(id, is_ap_id, verify_id) do
|
||||
Logger.debug("Dereferencing AP doc #{}")
|
||||
|
||||
with {:valid_uri_scheme, true} <- {:valid_uri_scheme, String.starts_with?(id, "http")},
|
||||
%URI{} = uri <- URI.parse(id),
|
||||
|
@ -270,18 +331,29 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
|||
{: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),
|
||||
# a canonical ID shouldn't be a redirect
|
||||
true <- !is_ap_id || final_id == id,
|
||||
{:ok, data} <- safe_json_decode(body),
|
||||
{_, :ok} <- {:strict_id, Containment.contain_id_to_fetch(final_id, data)},
|
||||
{_, :ok} <- {:containment, Containment.contain_origin(final_id, data)} do
|
||||
{_, :ok} <- {:containment, Containment.contain_origin(final_id, data)},
|
||||
{_, {:ok, _}} <- {:strict_id, verify_id.(final_id, data)} do
|
||||
unless Instances.reachable?(final_id) do
|
||||
Instances.set_reachable(final_id)
|
||||
end
|
||||
|
||||
{:ok, data}
|
||||
else
|
||||
{:strict_id, _} = e ->
|
||||
log_fetch_error(id, e)
|
||||
{:error, :id_mismatch}
|
||||
# E.g. Mastodon and *key serve the AP object directly under their display URLs without
|
||||
# redirecting to their canonical location first, thus ids will expectedly differ.
|
||||
# Similarly keys, either use a fragment ID and are a subobjects or a distinct ID
|
||||
# but for compatibility are still a subobject presenting their owning actors ID at the toplevel.
|
||||
# Refetching _once_ from the listed id, should yield a strict match afterwards.
|
||||
{:strict_id, {_error, ap_id}} = e ->
|
||||
if !is_ap_id and is_binary(ap_id) do
|
||||
fetch_and_contain_remote_ap_doc(ap_id, true, verify_id)
|
||||
else
|
||||
log_fetch_error(id, e)
|
||||
{:error, :id_mismatch}
|
||||
end
|
||||
|
||||
{:mrf_reject_check, _} = e ->
|
||||
log_fetch_error(id, e)
|
||||
|
@ -301,7 +373,7 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
|||
|
||||
{:containment, reason} ->
|
||||
log_fetch_error(id, reason)
|
||||
{:error, reason}
|
||||
{:error, {:containment, reason}}
|
||||
|
||||
{:error, e} ->
|
||||
{:error, e}
|
||||
|
@ -311,25 +383,10 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_and_contain_remote_object_from_id(_id),
|
||||
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
|
||||
|
@ -355,10 +412,6 @@ def get_object(id) do
|
|||
with {:ok, %{body: body, status: code, headers: headers, url: final_url}}
|
||||
when code in 200..299 <-
|
||||
HTTP.Backoff.get(id, headers),
|
||||
remote_host <-
|
||||
URI.parse(final_url).host,
|
||||
{:cross_domain_redirect, false} <-
|
||||
check_crossdomain_redirect(remote_host, id),
|
||||
{:has_content_type, {_, content_type}} <-
|
||||
{:has_content_type, List.keyfind(headers, "content-type", 0)},
|
||||
{:parse_content_type, {:ok, "application", subtype, type_params}} <-
|
||||
|
@ -369,8 +422,12 @@ def get_object(id) do
|
|||
{"activity+json", _} ->
|
||||
{:ok, final_id, body}
|
||||
|
||||
{"ld+json", %{"profile" => "https://www.w3.org/ns/activitystreams"}} ->
|
||||
{:ok, final_id, body}
|
||||
{"ld+json", %{"profile" => profiles}} ->
|
||||
if "https://www.w3.org/ns/activitystreams" in String.split(profiles) do
|
||||
{:ok, final_id, body}
|
||||
else
|
||||
{:error, {:content_type, content_type}}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, {:content_type, content_type}}
|
||||
|
|
216
lib/pleroma/safe_zip.ex
Normal file
216
lib/pleroma/safe_zip.ex
Normal file
|
@ -0,0 +1,216 @@
|
|||
# Akkoma: Magically expressive social media
|
||||
# Copyright © 2024 Akkoma Authors <https://akkoma.dev/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.SafeZip do
|
||||
@moduledoc """
|
||||
Wraps the subset of Erlang's zip module we’d like to use
|
||||
but enforces path-traversal safety everywhere and other checks.
|
||||
|
||||
For convenience almost all functions accept both elixir strings and charlists,
|
||||
but output elixir strings themselves. However, this means the input parameter type
|
||||
can no longer be used to distinguish archive file paths from archive binary data in memory,
|
||||
thus where needed both a _data and _file variant are provided.
|
||||
"""
|
||||
|
||||
@type text() :: String.t() | [char()]
|
||||
|
||||
defp is_safe_path?(path) do
|
||||
# Path accepts elixir’s chardata()
|
||||
case Path.safe_relative(path) do
|
||||
{:ok, _} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp is_safe_type?(file_type) do
|
||||
if file_type in [:regular, :directory] do
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_add_file(_type, _path_charlist, nil), do: nil
|
||||
|
||||
defp maybe_add_file(:regular, path_charlist, file_list),
|
||||
do: [to_string(path_charlist) | file_list]
|
||||
|
||||
defp maybe_add_file(_type, _path_charlist, file_list), do: file_list
|
||||
|
||||
@spec check_safe_archive_and_maybe_list_files(binary() | [char()], [term()], boolean()) ::
|
||||
{:ok, [String.t()]} | {:error, reason :: term()}
|
||||
defp check_safe_archive_and_maybe_list_files(archive, opts, list) do
|
||||
acc = if list, do: [], else: nil
|
||||
|
||||
with {:ok, table} <- :zip.table(archive, opts) do
|
||||
Enum.reduce_while(table, {:ok, acc}, fn
|
||||
# ZIP comment
|
||||
{:zip_comment, _}, acc ->
|
||||
{:cont, acc}
|
||||
|
||||
# File entry
|
||||
{:zip_file, path, info, _comment, _offset, _comp_size}, {:ok, fl} ->
|
||||
with {_, type} <- {:get_type, elem(info, 2)},
|
||||
{_, true} <- {:type, is_safe_type?(type)},
|
||||
{_, true} <- {:safe_path, is_safe_path?(path)} do
|
||||
{:cont, {:ok, maybe_add_file(type, path, fl)}}
|
||||
else
|
||||
{:get_type, e} ->
|
||||
{:halt,
|
||||
{:error, "Couldn't determine file type of ZIP entry at #{path} (#{inspect(e)})"}}
|
||||
|
||||
{:type, _} ->
|
||||
{:halt, {:error, "Potentially unsafe file type in ZIP at: #{path}"}}
|
||||
|
||||
{:safe_path, _} ->
|
||||
{:halt, {:error, "Unsafe path in ZIP: #{path}"}}
|
||||
end
|
||||
|
||||
# new OTP version?
|
||||
_, _acc ->
|
||||
{:halt, {:error, "Unknown ZIP record type"}}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_safe_archive_and_list_files(binary() | [char()], [term()]) ::
|
||||
{:ok, [String.t()]} | {:error, reason :: term()}
|
||||
defp check_safe_archive_and_list_files(archive, opts \\ []) do
|
||||
check_safe_archive_and_maybe_list_files(archive, opts, true)
|
||||
end
|
||||
|
||||
@spec check_safe_archive(binary() | [char()], [term()]) :: :ok | {:error, reason :: term()}
|
||||
defp check_safe_archive(archive, opts \\ []) do
|
||||
case check_safe_archive_and_maybe_list_files(archive, opts, false) do
|
||||
{:ok, _} -> :ok
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_safe_file_list([text()], text()) :: :ok | {:error, term()}
|
||||
defp check_safe_file_list([], _), do: :ok
|
||||
|
||||
defp check_safe_file_list([path | tail], cwd) do
|
||||
with {_, true} <- {:path, is_safe_path?(path)},
|
||||
{_, {:ok, fstat}} <- {:stat, File.stat(Path.expand(path, cwd))},
|
||||
{_, true} <- {:type, is_safe_type?(fstat.type)} do
|
||||
check_safe_file_list(tail, cwd)
|
||||
else
|
||||
{:path, _} ->
|
||||
{:error, "Unsafe path escaping cwd: #{path}"}
|
||||
|
||||
{:stat, e} ->
|
||||
{:error, "Unable to check file type of #{path}: #{inspect(e)}"}
|
||||
|
||||
{:type, _} ->
|
||||
{:error, "Unsafe type at #{path}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_safe_file_list(_, _), do: {:error, "Malformed file_list"}
|
||||
|
||||
@doc """
|
||||
Checks whether the archive data contais file entries for all paths from fset
|
||||
|
||||
Note this really only accepts entries corresponding to regular _files_,
|
||||
if a path is contained as for example an directory, this does not count as a match.
|
||||
"""
|
||||
@spec contains_all_data?(binary(), MapSet.t()) :: true | false
|
||||
def contains_all_data?(archive_data, fset) do
|
||||
with {:ok, table} <- :zip.table(archive_data) do
|
||||
remaining =
|
||||
Enum.reduce(table, fset, fn
|
||||
{:zip_file, path, info, _comment, _offset, _comp_size}, fset ->
|
||||
if elem(info, 2) == :regular do
|
||||
MapSet.delete(fset, path)
|
||||
else
|
||||
fset
|
||||
end
|
||||
|
||||
_, _ ->
|
||||
fset
|
||||
end)
|
||||
|> MapSet.size()
|
||||
|
||||
if remaining == 0, do: true, else: false
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
List all file entries in ZIP, or error if invalid or unsafe.
|
||||
|
||||
Note this really only lists regular files, no directories, ZIP comments or other types!
|
||||
"""
|
||||
@spec list_dir_file(text()) :: {:ok, [String.t()]} | {:error, reason :: term()}
|
||||
def list_dir_file(archive) do
|
||||
path = to_charlist(archive)
|
||||
check_safe_archive_and_list_files(path)
|
||||
end
|
||||
|
||||
defp stringify_zip({:ok, {fname, data}}), do: {:ok, {to_string(fname), data}}
|
||||
defp stringify_zip({:ok, fname}), do: {:ok, to_string(fname)}
|
||||
defp stringify_zip(ret), do: ret
|
||||
|
||||
@spec zip(text(), text(), [text()], boolean()) ::
|
||||
{:ok, file_name :: String.t()}
|
||||
| {:ok, {file_name :: String.t(), file_data :: binary()}}
|
||||
| {:error, reason :: term()}
|
||||
def zip(name, file_list, cwd, memory \\ false) do
|
||||
opts = [{:cwd, to_charlist(cwd)}]
|
||||
opts = if memory, do: [:memory | opts], else: opts
|
||||
|
||||
with :ok <- check_safe_file_list(file_list, cwd) do
|
||||
file_list = for f <- file_list, do: to_charlist(f)
|
||||
name = to_charlist(name)
|
||||
stringify_zip(:zip.zip(name, file_list, opts))
|
||||
end
|
||||
end
|
||||
|
||||
@spec unzip_file(text(), text(), [text()] | nil) ::
|
||||
{:ok, [String.t()]}
|
||||
| {:error, reason :: term()}
|
||||
| {:error, {name :: text(), reason :: term()}}
|
||||
def unzip_file(archive, target_dir, file_list \\ nil) do
|
||||
do_unzip(to_charlist(archive), to_charlist(target_dir), file_list)
|
||||
end
|
||||
|
||||
@spec unzip_data(binary(), text(), [text()] | nil) ::
|
||||
{:ok, [String.t()]}
|
||||
| {:error, reason :: term()}
|
||||
| {:error, {name :: text(), reason :: term()}}
|
||||
def unzip_data(archive, target_dir, file_list \\ nil) do
|
||||
do_unzip(archive, to_charlist(target_dir), file_list)
|
||||
end
|
||||
|
||||
defp stringify_unzip({:ok, [{_fname, _data} | _] = filebinlist}),
|
||||
do: {:ok, Enum.map(filebinlist, fn {fname, data} -> {to_string(fname), data} end)}
|
||||
|
||||
defp stringify_unzip({:ok, [_fname | _] = filelist}),
|
||||
do: {:ok, Enum.map(filelist, fn fname -> to_string(fname) end)}
|
||||
|
||||
defp stringify_unzip({:error, {fname, term}}), do: {:error, {to_string(fname), term}}
|
||||
defp stringify_unzip(ret), do: ret
|
||||
|
||||
@spec do_unzip(binary() | [char()], text(), [text()] | nil) ::
|
||||
{:ok, [String.t()]}
|
||||
| {:error, reason :: term()}
|
||||
| {:error, {name :: text(), reason :: term()}}
|
||||
defp do_unzip(archive, target_dir, file_list) do
|
||||
opts =
|
||||
if file_list != nil do
|
||||
[
|
||||
file_list: for(f <- file_list, do: to_charlist(f)),
|
||||
cwd: target_dir
|
||||
]
|
||||
else
|
||||
[cwd: target_dir]
|
||||
end
|
||||
|
||||
with :ok <- check_safe_archive(archive) do
|
||||
stringify_unzip(:zip.unzip(archive, opts))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -40,12 +40,6 @@ def search(user, search_query, options \\ []) do
|
|||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def add_to_index(_activity), do: nil
|
||||
|
||||
@impl true
|
||||
def remove_from_index(_object), do: nil
|
||||
|
||||
def maybe_restrict_author(query, %User{} = author) do
|
||||
Activity.Queries.by_author(query, author)
|
||||
end
|
||||
|
@ -95,21 +89,29 @@ defp query_with(q, :rum, search_query) do
|
|||
)
|
||||
end
|
||||
|
||||
def maybe_restrict_local(q, user) do
|
||||
def should_restrict_local(user) do
|
||||
limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
|
||||
|
||||
case {limit, user} do
|
||||
{:all, _} -> restrict_local(q)
|
||||
{:unauthenticated, %User{}} -> q
|
||||
{:unauthenticated, _} -> restrict_local(q)
|
||||
{false, _} -> q
|
||||
{:all, _} -> true
|
||||
{:unauthenticated, %User{}} -> false
|
||||
{:unauthenticated, _} -> true
|
||||
{false, _} -> false
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_restrict_local(q, user) do
|
||||
case should_restrict_local(user) do
|
||||
true -> restrict_local(q)
|
||||
false -> q
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_local(q), do: where(q, local: true)
|
||||
|
||||
def maybe_fetch(activities, user, search_query) do
|
||||
with true <- Regex.match?(~r/https?:/, search_query),
|
||||
with false <- should_restrict_local(user),
|
||||
true <- Regex.match?(~r/https?:/, search_query),
|
||||
{:ok, object} <- Fetcher.fetch_object_from_id(search_query),
|
||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
|
||||
true <- Visibility.visible_for_user?(activity, user) do
|
||||
|
|
|
@ -14,4 +14,6 @@ defmodule Pleroma.Search.SearchBackend do
|
|||
from index.
|
||||
"""
|
||||
@callback remove_from_index(object :: Pleroma.Object.t()) :: {:ok, any()} | {:error, any()}
|
||||
|
||||
@optional_callbacks add_to_index: 1, remove_from_index: 1
|
||||
end
|
||||
|
|
|
@ -5,71 +5,52 @@
|
|||
defmodule Pleroma.Signature do
|
||||
@behaviour HTTPSignatures.Adapter
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
||||
@known_suffixes ["/publickey", "/main-key", "#key"]
|
||||
|
||||
def key_id_to_actor_id(key_id) do
|
||||
uri =
|
||||
key_id
|
||||
|> URI.parse()
|
||||
|> Map.put(:fragment, nil)
|
||||
|> Map.put(:query, nil)
|
||||
|> remove_suffix(@known_suffixes)
|
||||
|
||||
maybe_ap_id = URI.to_string(uri)
|
||||
|
||||
case ObjectValidators.ObjectID.cast(maybe_ap_id) do
|
||||
{:ok, ap_id} ->
|
||||
{:ok, ap_id}
|
||||
|
||||
_ ->
|
||||
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
|
||||
{:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id}
|
||||
_ -> {:error, maybe_ap_id}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_suffix(uri, [test | rest]) do
|
||||
if not is_nil(uri.path) and String.ends_with?(uri.path, test) do
|
||||
Map.put(uri, :path, String.replace(uri.path, test, ""))
|
||||
else
|
||||
remove_suffix(uri, rest)
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_suffix(uri, []), do: uri
|
||||
alias Pleroma.User.SigningKey
|
||||
require Logger
|
||||
|
||||
def fetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||
{:ok, public_key}
|
||||
with {_, %{"keyId" => kid}} <- {:keyid, HTTPSignatures.signature_for_conn(conn)},
|
||||
{_, {:ok, %SigningKey{} = sk}, _} <-
|
||||
{:fetch, SigningKey.get_or_fetch_by_key_id(kid), kid},
|
||||
{_, {:ok, decoded_key}} <- {:decode, SigningKey.public_key_decoded(sk)} do
|
||||
{:ok, decoded_key}
|
||||
else
|
||||
{:fetch, error, kid} ->
|
||||
Logger.error("Failed to acquire key from signature: #{kid} #{inspect(error)}")
|
||||
{:error, {:fetch, error}}
|
||||
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def refetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, actor_id} <- key_id_to_actor_id(kid),
|
||||
{:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
|
||||
{:ok, public_key}
|
||||
with {_, %{"keyId" => kid}} <- {:keyid, HTTPSignatures.signature_for_conn(conn)},
|
||||
{_, {:ok, %SigningKey{} = sk}, _} <- {:fetch, SigningKey.refresh_by_key_id(kid), kid},
|
||||
{_, {:ok, decoded_key}} <- {:decode, SigningKey.public_key_decoded(sk)} do
|
||||
{:ok, decoded_key}
|
||||
else
|
||||
{:fetch, {:error, :too_young}, kid} ->
|
||||
Logger.debug("Refusing to refetch recently updated key: #{kid}")
|
||||
{:error, {:fetch, :too_young}}
|
||||
|
||||
{:fetch, {:error, :unknown}, kid} ->
|
||||
Logger.warning("Attempted to refresh unknown key; this should not happen: #{kid}")
|
||||
{:error, {:fetch, :unknown}}
|
||||
|
||||
{:fetch, error, kid} ->
|
||||
Logger.error("Failed to refresh stale key from signature: #{kid} #{inspect(error)}")
|
||||
{:error, {:fetch, error}}
|
||||
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def sign(%User{keys: keys} = user, headers) do
|
||||
with {:ok, private_key, _} <- Keys.keys_from_pem(keys) do
|
||||
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
|
||||
def sign(%User{} = user, headers) do
|
||||
with {:ok, private_key} <- SigningKey.private_key(user) do
|
||||
HTTPSignatures.sign(private_key, SigningKey.local_key_id(user.ap_id), headers)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Stats do
|
|||
alias Pleroma.CounterCache
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Instances.Instance
|
||||
|
||||
@interval :timer.seconds(300)
|
||||
|
||||
|
@ -39,7 +40,8 @@ def force_update do
|
|||
@spec get_stats() :: %{
|
||||
domain_count: non_neg_integer(),
|
||||
status_count: non_neg_integer(),
|
||||
user_count: non_neg_integer()
|
||||
user_count: non_neg_integer(),
|
||||
remote_user_count: non_neg_integer()
|
||||
}
|
||||
def get_stats do
|
||||
%{stats: stats} = GenServer.call(__MODULE__, :get_state)
|
||||
|
@ -60,41 +62,39 @@ def get_peers do
|
|||
stats: %{
|
||||
domain_count: non_neg_integer(),
|
||||
status_count: non_neg_integer(),
|
||||
user_count: non_neg_integer()
|
||||
user_count: non_neg_integer(),
|
||||
remote_user_count: non_neg_integer()
|
||||
}
|
||||
}
|
||||
def calculate_stat_data do
|
||||
# instances table has an unique constraint on the host column
|
||||
peers =
|
||||
from(
|
||||
u in User,
|
||||
select: fragment("distinct split_part(?, '@', 2)", u.nickname),
|
||||
where: u.local != ^true
|
||||
i in Instance,
|
||||
select: i.host
|
||||
)
|
||||
|> Repo.all()
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
domain_count = Enum.count(peers)
|
||||
|
||||
status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count)
|
||||
|
||||
users_query =
|
||||
# there are few enough local users for postgres to use an index scan
|
||||
# (also here an exact count is a bit more important)
|
||||
user_count =
|
||||
from(u in User,
|
||||
where: u.is_active == true,
|
||||
where: u.local == true,
|
||||
where: not is_nil(u.nickname),
|
||||
where: not u.invisible
|
||||
)
|
||||
|> Repo.aggregate(:count, :id)
|
||||
|
||||
remote_users_query =
|
||||
from(u in User,
|
||||
where: u.is_active == true,
|
||||
where: u.local == false,
|
||||
where: not is_nil(u.nickname),
|
||||
where: not u.invisible
|
||||
)
|
||||
|
||||
user_count = Repo.aggregate(users_query, :count, :id)
|
||||
remote_user_count = Repo.aggregate(remote_users_query, :count, :id)
|
||||
# but mostly numerous remote users leading to a full a full table scan
|
||||
# (ecto currently doesn't allow building queries without explicit table)
|
||||
%{rows: [[remote_user_count]]} =
|
||||
"SELECT estimate_remote_user_count();"
|
||||
|> Pleroma.Repo.query!()
|
||||
|
||||
%{
|
||||
peers: peers,
|
||||
|
|
|
@ -18,6 +18,7 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripMetadata do
|
|||
|
||||
# Formats not compatible with exiftool at this time
|
||||
def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop}
|
||||
def filter(%Pleroma.Upload{content_type: "image/bmp"}), do: {:ok, :noop}
|
||||
def filter(%Pleroma.Upload{content_type: "image/svg+xml"}), do: {:ok, :noop}
|
||||
|
||||
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
|
||||
|
|
|
@ -25,7 +25,6 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Hashtag
|
||||
alias Pleroma.User.HashtagFollow
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.MFA
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
|
@ -43,6 +42,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Web.OAuth
|
||||
alias Pleroma.Web.RelMe
|
||||
alias Pleroma.Workers.BackgroundWorker
|
||||
alias Pleroma.User.SigningKey
|
||||
|
||||
use Pleroma.Web, :verified_routes
|
||||
|
||||
|
@ -100,8 +100,6 @@ defmodule Pleroma.User do
|
|||
field(:password_hash, :string)
|
||||
field(:password, :string, virtual: true)
|
||||
field(:password_confirmation, :string, virtual: true)
|
||||
field(:keys, :string)
|
||||
field(:public_key, :string)
|
||||
field(:ap_id, :string)
|
||||
field(:avatar, :map, default: %{})
|
||||
field(:local, :boolean, default: true)
|
||||
|
@ -128,7 +126,6 @@ defmodule Pleroma.User do
|
|||
field(:domain_blocks, {:array, :string}, default: [])
|
||||
field(:is_active, :boolean, default: true)
|
||||
field(:no_rich_text, :boolean, default: false)
|
||||
field(:ap_enabled, :boolean, default: false)
|
||||
field(:is_moderator, :boolean, default: false)
|
||||
field(:is_admin, :boolean, default: false)
|
||||
field(:show_role, :boolean, default: true)
|
||||
|
@ -222,6 +219,12 @@ defmodule Pleroma.User do
|
|||
on_replace: :delete
|
||||
)
|
||||
|
||||
# FOR THE FUTURE: We might want to make this a one-to-many relationship
|
||||
# it's entirely possible right now, but we don't have a use case for it
|
||||
# XXX: in the future we’d also like to parse and honour key expiration times
|
||||
# instead of blindly accepting any change in signing keys
|
||||
has_one(:signing_key, SigningKey, foreign_key: :user_id, on_replace: :update)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
|
@ -440,6 +443,7 @@ defp fix_follower_address(params), do: params
|
|||
def remote_user_changeset(struct \\ %User{local: false}, params) do
|
||||
bio_limit = Config.get([:instance, :user_bio_length], 5000)
|
||||
name_limit = Config.get([:instance, :user_name_length], 100)
|
||||
fields_limit = Config.get([:instance, :max_remote_account_fields], 0)
|
||||
|
||||
name =
|
||||
case params[:name] do
|
||||
|
@ -453,10 +457,12 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|
|||
|> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
|
||||
|> truncate_if_exists(:name, name_limit)
|
||||
|> truncate_if_exists(:bio, bio_limit)
|
||||
|> Map.update(:fields, [], &Enum.take(&1, fields_limit))
|
||||
|> truncate_fields_param()
|
||||
|> fix_follower_address()
|
||||
|
||||
struct
|
||||
|> Repo.preload(:signing_key)
|
||||
|> cast(
|
||||
params,
|
||||
[
|
||||
|
@ -466,9 +472,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|
|||
:inbox,
|
||||
:shared_inbox,
|
||||
:nickname,
|
||||
:public_key,
|
||||
:avatar,
|
||||
:ap_enabled,
|
||||
:banner,
|
||||
:background,
|
||||
:is_locked,
|
||||
|
@ -495,6 +499,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
|
|||
|> validate_required([:ap_id])
|
||||
|> validate_required([:name], trim: false)
|
||||
|> unique_constraint(:nickname)
|
||||
|> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, required: false)
|
||||
|> validate_format(:nickname, @email_regex)
|
||||
|> validate_length(:bio, max: bio_limit)
|
||||
|> validate_length(:name, max: name_limit)
|
||||
|
@ -526,7 +531,6 @@ def update_changeset(struct, params \\ %{}) do
|
|||
:name,
|
||||
:emoji,
|
||||
:avatar,
|
||||
:public_key,
|
||||
:inbox,
|
||||
:shared_inbox,
|
||||
:is_locked,
|
||||
|
@ -570,6 +574,7 @@ def update_changeset(struct, params \\ %{}) do
|
|||
:pleroma_settings_store,
|
||||
&{:ok, Map.merge(struct.pleroma_settings_store, &1)}
|
||||
)
|
||||
|> cast_assoc(:signing_key, with: &SigningKey.remote_changeset/2, requred: false)
|
||||
|> validate_fields(false, struct)
|
||||
end
|
||||
|
||||
|
@ -828,8 +833,10 @@ def put_following_and_follower_and_featured_address(changeset) do
|
|||
end
|
||||
|
||||
defp put_private_key(changeset) do
|
||||
{:ok, pem} = Keys.generate_rsa_pem()
|
||||
put_change(changeset, :keys, pem)
|
||||
ap_id = get_field(changeset, :ap_id)
|
||||
|
||||
changeset
|
||||
|> put_assoc(:signing_key, SigningKey.generate_local_keys(ap_id))
|
||||
end
|
||||
|
||||
defp autofollow_users(user) do
|
||||
|
@ -998,11 +1005,7 @@ def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
|
|||
end
|
||||
|
||||
def maybe_direct_follow(%User{} = follower, %User{} = followed) do
|
||||
if not ap_enabled?(followed) do
|
||||
follow(follower, followed)
|
||||
else
|
||||
{:ok, follower, followed}
|
||||
end
|
||||
{:ok, follower, followed}
|
||||
end
|
||||
|
||||
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
|
||||
|
@ -1146,7 +1149,8 @@ def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
|
|||
was_superuser_before_update = User.superuser?(user)
|
||||
|
||||
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
|
||||
set_cache(user)
|
||||
user
|
||||
|> set_cache()
|
||||
end
|
||||
|> maybe_remove_report_notifications(was_superuser_before_update)
|
||||
end
|
||||
|
@ -1817,7 +1821,6 @@ def purge_user_changeset(user) do
|
|||
confirmation_token: nil,
|
||||
domain_blocks: [],
|
||||
is_active: false,
|
||||
ap_enabled: false,
|
||||
is_moderator: false,
|
||||
is_admin: false,
|
||||
mastofe_settings: nil,
|
||||
|
@ -1997,8 +2000,20 @@ def get_or_fetch_by_ap_id(ap_id, options \\ []) do
|
|||
{%User{} = user, _} ->
|
||||
{:ok, user}
|
||||
|
||||
e ->
|
||||
{_, {:error, {:reject, :mrf}}} ->
|
||||
Logger.debug("Rejected to fetch user due to MRF: #{ap_id}")
|
||||
{:error, {:reject, :mrf}}
|
||||
|
||||
{_, {:error, :not_found}} ->
|
||||
Logger.debug("User doesn't exist (anymore): #{ap_id}")
|
||||
{:error, :not_found}
|
||||
|
||||
{_, {:error, e}} ->
|
||||
Logger.error("Could not fetch user #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
e ->
|
||||
Logger.error("Unexpected error condition while fetching user #{ap_id}, #{inspect(e)}")
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
@ -2051,30 +2066,7 @@ defp create_service_actor(uri, nickname) do
|
|||
|> set_cache()
|
||||
end
|
||||
|
||||
def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
|
||||
key =
|
||||
public_key_pem
|
||||
|> :public_key.pem_decode()
|
||||
|> hd()
|
||||
|> :public_key.pem_entry_decode()
|
||||
|
||||
{:ok, key}
|
||||
end
|
||||
|
||||
def public_key(_), do: {:error, "key not found"}
|
||||
|
||||
def get_public_key_for_ap_id(ap_id) do
|
||||
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
|
||||
{:ok, public_key} <- public_key(user) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def ap_enabled?(%User{local: true}), do: true
|
||||
def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
|
||||
def ap_enabled?(_), do: false
|
||||
defdelegate public_key(user), to: SigningKey
|
||||
|
||||
@doc "Gets or fetch a user by uri or nickname."
|
||||
@spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
|
||||
|
@ -2579,10 +2571,10 @@ def add_pinned_object_id(%User{} = user, object_id) do
|
|||
[pinned_objects: "You have already pinned the maximum number of statuses"]
|
||||
end
|
||||
end)
|
||||
|> update_and_set_cache()
|
||||
else
|
||||
change(user)
|
||||
{:ok, user}
|
||||
end
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
@spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
|
||||
|
|
|
@ -119,7 +119,7 @@ def process(%__MODULE__{} = backup) do
|
|||
end
|
||||
end
|
||||
|
||||
@files [~c"actor.json", ~c"outbox.json", ~c"likes.json", ~c"bookmarks.json"]
|
||||
@files ["actor.json", "outbox.json", "likes.json", "bookmarks.json"]
|
||||
def export(%__MODULE__{} = backup) do
|
||||
backup = Repo.preload(backup, :user)
|
||||
name = String.trim_trailing(backup.file_name, ".zip")
|
||||
|
@ -130,10 +130,9 @@ def export(%__MODULE__{} = backup) do
|
|||
:ok <- statuses(dir, backup.user),
|
||||
:ok <- likes(dir, backup.user),
|
||||
:ok <- bookmarks(dir, backup.user),
|
||||
{:ok, zip_path} <-
|
||||
:zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: String.to_charlist(dir)),
|
||||
{:ok, zip_path} <- Pleroma.SafeZip.zip(dir <> ".zip", @files, dir),
|
||||
{:ok, _} <- File.rm_rf(dir) do
|
||||
{:ok, to_string(zip_path)}
|
||||
{:ok, zip_path}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
268
lib/pleroma/user/signing_key.ex
Normal file
268
lib/pleroma/user/signing_key.ex
Normal file
|
@ -0,0 +1,268 @@
|
|||
defmodule Pleroma.User.SigningKey do
|
||||
use Ecto.Schema
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
require Pleroma.Constants
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Repo
|
||||
|
||||
require Logger
|
||||
|
||||
@primary_key false
|
||||
schema "signing_keys" do
|
||||
belongs_to(:user, Pleroma.User, type: FlakeId.Ecto.CompatType)
|
||||
field :public_key, :string
|
||||
field :private_key, :string
|
||||
# This is an arbitrary field given by the remote instance
|
||||
field :key_id, :string, primary_key: true
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def load_key(%User{} = user) do
|
||||
user
|
||||
|> Repo.preload(:signing_key)
|
||||
end
|
||||
|
||||
def key_id_of_local_user(%User{local: true} = user) do
|
||||
case Repo.preload(user, :signing_key) do
|
||||
%User{signing_key: %__MODULE__{key_id: key_id}} -> key_id
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec remote_changeset(__MODULE__, map) :: Changeset.t()
|
||||
def remote_changeset(%__MODULE__{} = signing_key, attrs) do
|
||||
signing_key
|
||||
|> cast(attrs, [:public_key, :key_id])
|
||||
|> validate_required([:public_key, :key_id])
|
||||
end
|
||||
|
||||
@spec key_id_to_user_id(String.t()) :: String.t() | nil
|
||||
@doc """
|
||||
Given a key ID, return the user ID associated with that key.
|
||||
Returns nil if the key ID is not found.
|
||||
"""
|
||||
def key_id_to_user_id(key_id) do
|
||||
from(sk in __MODULE__, where: sk.key_id == ^key_id)
|
||||
|> select([sk], sk.user_id)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@spec key_id_to_ap_id(String.t()) :: String.t() | nil
|
||||
@doc """
|
||||
Given a key ID, return the AP ID associated with that key.
|
||||
Returns nil if the key ID is not found.
|
||||
"""
|
||||
def key_id_to_ap_id(key_id) do
|
||||
Logger.debug("Looking up key ID: #{key_id}")
|
||||
|
||||
from(sk in __MODULE__, where: sk.key_id == ^key_id)
|
||||
|> join(:inner, [sk], u in User, on: sk.user_id == u.id)
|
||||
|> select([sk, u], u.ap_id)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@spec generate_rsa_pem() :: {:ok, binary()}
|
||||
@doc """
|
||||
Generate a new RSA private key and return it as a PEM-encoded string.
|
||||
"""
|
||||
def generate_rsa_pem do
|
||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
|
||||
{:ok, pem}
|
||||
end
|
||||
|
||||
@spec generate_local_keys(String.t()) :: {:ok, Changeset.t()} | {:error, String.t()}
|
||||
@doc """
|
||||
Generate a new RSA key pair and create a changeset for it
|
||||
"""
|
||||
def generate_local_keys(ap_id) do
|
||||
{:ok, private_pem} = generate_rsa_pem()
|
||||
{:ok, local_pem} = private_pem_to_public_pem(private_pem)
|
||||
|
||||
%__MODULE__{}
|
||||
|> change()
|
||||
|> put_change(:public_key, local_pem)
|
||||
|> put_change(:private_key, private_pem)
|
||||
|> put_change(:key_id, local_key_id(ap_id))
|
||||
end
|
||||
|
||||
@spec local_key_id(String.t()) :: String.t()
|
||||
@doc """
|
||||
Given an AP ID, return the key ID for the local user.
|
||||
"""
|
||||
def local_key_id(ap_id) do
|
||||
ap_id <> "#main-key"
|
||||
end
|
||||
|
||||
@spec private_pem_to_public_pem(binary) :: {:ok, binary()} | {:error, String.t()}
|
||||
@doc """
|
||||
Given a private key in PEM format, return the corresponding public key in PEM format.
|
||||
"""
|
||||
def private_pem_to_public_pem(private_pem) do
|
||||
[private_key_code] = :public_key.pem_decode(private_pem)
|
||||
private_key = :public_key.pem_entry_decode(private_key_code)
|
||||
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
|
||||
public_key = {:RSAPublicKey, modulus, exponent}
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
{:ok, :public_key.pem_encode([public_key])}
|
||||
end
|
||||
|
||||
@spec public_key(__MODULE__) :: {:ok, binary()} | {:error, String.t()}
|
||||
@doc """
|
||||
Return public key data in binary format.
|
||||
"""
|
||||
def public_key_decoded(%__MODULE__{public_key: public_key_pem}) do
|
||||
decoded =
|
||||
public_key_pem
|
||||
|> :public_key.pem_decode()
|
||||
|> hd()
|
||||
|> :public_key.pem_entry_decode()
|
||||
|
||||
{:ok, decoded}
|
||||
end
|
||||
|
||||
def public_key(_), do: {:error, "key not found"}
|
||||
|
||||
def public_key_pem(%User{} = user) do
|
||||
case Repo.preload(user, :signing_key) do
|
||||
%User{signing_key: %__MODULE__{public_key: public_key_pem}} -> {:ok, public_key_pem}
|
||||
_ -> {:error, "key not found"}
|
||||
end
|
||||
end
|
||||
|
||||
def public_key_pem(_e) do
|
||||
{:error, "key not found"}
|
||||
end
|
||||
|
||||
@spec private_key(User.t()) :: {:ok, binary()} | {:error, String.t()}
|
||||
@doc """
|
||||
Given a user, return the private key for that user in binary format.
|
||||
"""
|
||||
def private_key(%User{} = user) do
|
||||
case Repo.preload(user, :signing_key) do
|
||||
%{signing_key: %__MODULE__{private_key: private_key_pem}} ->
|
||||
key =
|
||||
private_key_pem
|
||||
|> :public_key.pem_decode()
|
||||
|> hd()
|
||||
|> :public_key.pem_entry_decode()
|
||||
|
||||
{:ok, key}
|
||||
|
||||
_ ->
|
||||
{:error, "key not found"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_or_fetch_by_key_id(String.t()) :: {:ok, __MODULE__} | {:error, String.t()}
|
||||
@doc """
|
||||
Given a key ID, return the signing key associated with that key.
|
||||
Will either return the key if it exists locally, or fetch it from the remote instance.
|
||||
"""
|
||||
def get_or_fetch_by_key_id(key_id) do
|
||||
case Repo.get_by(__MODULE__, key_id: key_id) do
|
||||
nil ->
|
||||
fetch_remote_key(key_id)
|
||||
|
||||
key ->
|
||||
{:ok, key}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_remote_key(String.t()) :: {:ok, __MODULE__} | {:error, String.t()}
|
||||
@doc """
|
||||
Fetch a remote key by key ID.
|
||||
Will send a request to the remote instance to get the key ID.
|
||||
This request should, at the very least, return a user ID and a public key object.
|
||||
Though bear in mind that some implementations (looking at you, pleroma) may require a signature for this request.
|
||||
This has the potential to create an infinite loop if the remote instance requires a signature to fetch the key...
|
||||
So if we're rejected, we should probably just give up.
|
||||
"""
|
||||
def fetch_remote_key(key_id) do
|
||||
Logger.debug("Fetching remote key: #{key_id}")
|
||||
|
||||
with {:ok, resp_body} <-
|
||||
Pleroma.Object.Fetcher.fetch_and_contain_remote_key(key_id),
|
||||
{:ok, ap_id, public_key_pem} <- handle_signature_response(resp_body),
|
||||
{:ok, user} <- User.get_or_fetch_by_ap_id(ap_id) do
|
||||
Logger.debug("Fetched remote key: #{ap_id}")
|
||||
# store the key
|
||||
key = %{
|
||||
user_id: user.id,
|
||||
public_key: public_key_pem,
|
||||
key_id: key_id
|
||||
}
|
||||
|
||||
key_cs =
|
||||
cast(%__MODULE__{}, key, [:user_id, :public_key, :key_id])
|
||||
|> unique_constraint(:user_id)
|
||||
|
||||
Repo.insert(key_cs,
|
||||
# while this should never run for local users anyway, etc make sure we really never loose privkey info!
|
||||
on_conflict: {:replace_all_except, [:inserted_at, :private_key]},
|
||||
# if the key owner overlaps with a distinct existing key entry, this intetionally still errros
|
||||
conflict_target: :key_id
|
||||
)
|
||||
else
|
||||
e ->
|
||||
Logger.debug("Failed to fetch remote key: #{inspect(e)}")
|
||||
{:error, "Could not fetch key"}
|
||||
end
|
||||
end
|
||||
|
||||
defp refresh_key(%__MODULE__{} = key) do
|
||||
min_backoff = Pleroma.Config.get!([:activitypub, :min_key_refetch_interval])
|
||||
|
||||
if Timex.diff(Timex.now(), key.updated_at, :seconds) >= min_backoff do
|
||||
fetch_remote_key(key.key_id)
|
||||
else
|
||||
{:error, :too_young}
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_by_key_id(key_id) do
|
||||
case Repo.get_by(__MODULE__, key_id: key_id) do
|
||||
nil -> {:error, :unknown}
|
||||
key -> refresh_key(key)
|
||||
end
|
||||
end
|
||||
|
||||
# Take the response from the remote instance and extract the key details
|
||||
# will check if the key ID matches the owner of the key, if not, error
|
||||
defp extract_key_details(%{"id" => ap_id, "publicKey" => public_key}) do
|
||||
if ap_id !== public_key["owner"] do
|
||||
{:error, "Key ID does not match owner"}
|
||||
else
|
||||
%{"publicKeyPem" => public_key_pem} = public_key
|
||||
{:ok, ap_id, public_key_pem}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_signature_response(body) do
|
||||
case body do
|
||||
%{
|
||||
"type" => "CryptographicKey",
|
||||
"publicKeyPem" => public_key_pem,
|
||||
"owner" => ap_id
|
||||
} ->
|
||||
{:ok, ap_id, public_key_pem}
|
||||
|
||||
# for when we get a subset of the user object
|
||||
%{
|
||||
"id" => _user_id,
|
||||
"publicKey" => _public_key,
|
||||
"type" => actor_type
|
||||
}
|
||||
when actor_type in Pleroma.Constants.actor_types() ->
|
||||
extract_key_details(body)
|
||||
|
||||
%{"error" => error} ->
|
||||
{:error, error}
|
||||
|
||||
other ->
|
||||
{:error, "Could not process key: #{inspect(other)}"}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1547,6 +1547,17 @@ defp normalize_attachment(%{} = attachment), do: [attachment]
|
|||
defp normalize_attachment(attachment) when is_list(attachment), do: attachment
|
||||
defp normalize_attachment(_), do: []
|
||||
|
||||
defp maybe_make_public_key_object(data) do
|
||||
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
|
||||
%{
|
||||
public_key: data["publicKey"]["publicKeyPem"],
|
||||
key_id: data["publicKey"]["id"]
|
||||
}
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp object_to_user_data(data, additional) do
|
||||
fields =
|
||||
data
|
||||
|
@ -1578,9 +1589,16 @@ defp object_to_user_data(data, additional) do
|
|||
featured_address = data["featured"]
|
||||
{:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address)
|
||||
|
||||
public_key =
|
||||
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
|
||||
data["publicKey"]["publicKeyPem"]
|
||||
# first, check that the owner is correct
|
||||
signing_key =
|
||||
if data["id"] !== data["publicKey"]["owner"] do
|
||||
Logger.error(
|
||||
"Owner of the public key is not the same as the actor - not saving the public key."
|
||||
)
|
||||
|
||||
nil
|
||||
else
|
||||
maybe_make_public_key_object(data)
|
||||
end
|
||||
|
||||
shared_inbox =
|
||||
|
@ -1608,7 +1626,6 @@ defp object_to_user_data(data, additional) do
|
|||
%{
|
||||
ap_id: data["id"],
|
||||
uri: get_actor_url(data["url"]),
|
||||
ap_enabled: true,
|
||||
banner: normalize_image(data["image"]),
|
||||
background: normalize_image(data["backgroundUrl"]),
|
||||
fields: fields,
|
||||
|
@ -1624,7 +1641,7 @@ defp object_to_user_data(data, additional) do
|
|||
bio: data["summary"] || "",
|
||||
actor_type: actor_type,
|
||||
also_known_as: also_known_as,
|
||||
public_key: public_key,
|
||||
signing_key: signing_key,
|
||||
inbox: data["inbox"],
|
||||
shared_inbox: shared_inbox,
|
||||
pinned_objects: pinned_objects,
|
||||
|
@ -1725,7 +1742,7 @@ def user_data_from_user_object(data, additional \\ []) do
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do
|
||||
defp fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
|
||||
{:valid, {:ok, _, _}} <- {:valid, UserValidator.validate(data, [])},
|
||||
{:ok, data} <- user_data_from_user_object(data, additional) do
|
||||
|
@ -1733,19 +1750,16 @@ def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do
|
|||
else
|
||||
# If this has been deleted, only log a debug and not an error
|
||||
{:error, {"Object has been deleted", _, _} = e} ->
|
||||
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
Logger.debug("User was explicitly deleted #{ap_id}, #{inspect(e)}")
|
||||
{:error, :not_found}
|
||||
|
||||
{:reject, reason} = e ->
|
||||
Logger.debug("Rejected user #{ap_id}: #{inspect(reason)}")
|
||||
{:reject, _reason} = e ->
|
||||
{:error, e}
|
||||
|
||||
{:valid, reason} ->
|
||||
Logger.debug("Data is not a valid user #{ap_id}: #{inspect(reason)}")
|
||||
{:error, "Not a user"}
|
||||
{:error, {:validate, reason}}
|
||||
|
||||
{:error, e} ->
|
||||
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
@ -1783,7 +1797,7 @@ def pin_data_from_featured_collection(%{
|
|||
else
|
||||
e ->
|
||||
Logger.error("Could not decode featured collection at fetch #{first}, #{inspect(e)}")
|
||||
{:ok, %{}}
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1793,14 +1807,18 @@ def pin_data_from_featured_collection(
|
|||
} = collection
|
||||
)
|
||||
when type in ["OrderedCollection", "Collection"] do
|
||||
{:ok, objects} = Collections.Fetcher.fetch_collection(collection)
|
||||
|
||||
# Items can either be a map _or_ a string
|
||||
objects
|
||||
|> Map.new(fn
|
||||
ap_id when is_binary(ap_id) -> {ap_id, NaiveDateTime.utc_now()}
|
||||
%{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()}
|
||||
end)
|
||||
with {:ok, objects} <- Collections.Fetcher.fetch_collection(collection) do
|
||||
# Items can either be a map _or_ a string
|
||||
objects
|
||||
|> Map.new(fn
|
||||
ap_id when is_binary(ap_id) -> {ap_id, NaiveDateTime.utc_now()}
|
||||
%{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()}
|
||||
end)
|
||||
else
|
||||
e ->
|
||||
Logger.warning("Failed to fetch featured collection #{collection}, #{inspect(e)}")
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
def pin_data_from_featured_collection(obj) do
|
||||
|
@ -1839,31 +1857,27 @@ def enqueue_pin_fetches(_), do: nil
|
|||
def make_user_from_ap_id(ap_id, additional \\ []) do
|
||||
user = User.get_cached_by_ap_id(ap_id)
|
||||
|
||||
if user && !User.ap_enabled?(user) do
|
||||
Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
||||
else
|
||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||
user =
|
||||
if data.ap_id != ap_id do
|
||||
User.get_cached_by_ap_id(data.ap_id)
|
||||
else
|
||||
user
|
||||
end
|
||||
|
||||
if user do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
|> tap(fn _ -> enqueue_pin_fetches(data) end)
|
||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||
user =
|
||||
if data.ap_id != ap_id do
|
||||
User.get_cached_by_ap_id(data.ap_id)
|
||||
else
|
||||
maybe_handle_clashing_nickname(data)
|
||||
|
||||
data
|
||||
|> User.remote_user_changeset()
|
||||
|> Repo.insert()
|
||||
|> User.set_cache()
|
||||
|> tap(fn _ -> enqueue_pin_fetches(data) end)
|
||||
user
|
||||
end
|
||||
|
||||
if user do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
|> tap(fn _ -> enqueue_pin_fetches(data) end)
|
||||
else
|
||||
maybe_handle_clashing_nickname(data)
|
||||
|
||||
data
|
||||
|> User.remote_user_changeset()
|
||||
|> Repo.insert()
|
||||
|> User.set_cache()
|
||||
|> tap(fn _ -> enqueue_pin_fetches(data) end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -60,7 +60,26 @@ defp relay_active?(conn, _) do
|
|||
end
|
||||
end
|
||||
|
||||
def user(conn, %{"nickname" => nickname}) do
|
||||
@doc """
|
||||
Render the user's AP data
|
||||
WARNING: we cannot actually check if the request has a fragment! so let's play defensively
|
||||
- IF we have a valid signature, serve full user
|
||||
- IF we do not, and authorized_fetch_mode is enabled, serve the key only
|
||||
- OTHERWISE, serve the full actor (since we don't need to worry about the signature)
|
||||
"""
|
||||
def user(%{assigns: %{valid_signature: true}} = conn, params) do
|
||||
render_full_user(conn, params)
|
||||
end
|
||||
|
||||
def user(conn, params) do
|
||||
if Pleroma.Config.get([:activitypub, :authorized_fetch_mode], false) do
|
||||
render_key_only_user(conn, params)
|
||||
else
|
||||
render_full_user(conn, params)
|
||||
end
|
||||
end
|
||||
|
||||
defp render_full_user(conn, %{"nickname" => nickname}) do
|
||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|
@ -72,6 +91,18 @@ def user(conn, %{"nickname" => nickname}) do
|
|||
end
|
||||
end
|
||||
|
||||
def render_key_only_user(conn, %{"nickname" => nickname}) do
|
||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||
conn
|
||||
|> put_resp_content_type("application/activity+json")
|
||||
|> put_view(UserView)
|
||||
|> render("keys.json", %{user: user})
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
%{local: false} -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def object(%{assigns: assigns} = conn, _) do
|
||||
with ap_id <- Endpoint.url() <> conn.request_path,
|
||||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
|
||||
|
|
|
@ -24,7 +24,11 @@ defp score_displayname("federationbot"), do: 1.0
|
|||
defp score_displayname("fedibot"), do: 1.0
|
||||
defp score_displayname(_), do: 0.0
|
||||
|
||||
defp determine_if_followbot(%User{nickname: nickname, name: displayname, actor_type: actor_type}) do
|
||||
defp determine_if_followbot(%User{
|
||||
nickname: nickname,
|
||||
name: displayname,
|
||||
actor_type: actor_type
|
||||
}) do
|
||||
# nickname will be a binary string except when following a relay
|
||||
nick_score =
|
||||
if is_binary(nickname) do
|
||||
|
|
|
@ -34,16 +34,34 @@ defp check_reject(message, actions) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec delete_and_count(list(), term()) :: {integer(), list()}
|
||||
defp delete_and_count(list, element), do: delete_and_count(list, element, {0, [], list})
|
||||
|
||||
defp delete_and_count([], _element, {0, _nlist, olist}), do: {0, olist}
|
||||
defp delete_and_count([], _element, {count, nlist, _olist}), do: {count, Enum.reverse(nlist)}
|
||||
|
||||
defp delete_and_count([h | r], h, {count, nlist, olist}),
|
||||
do: delete_and_count(r, h, {count + 1, nlist, olist})
|
||||
|
||||
defp delete_and_count([h | r], element, {count, nlist, olist}),
|
||||
do: delete_and_count(r, element, {count, [h | nlist], olist})
|
||||
|
||||
defp insert_if_needed(list, oldcount, element) do
|
||||
if oldcount <= 0 || Enum.member?(list, element) do
|
||||
list
|
||||
else
|
||||
[element | list]
|
||||
end
|
||||
end
|
||||
|
||||
defp check_delist(message, actions) do
|
||||
if :delist in actions do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
|
||||
to =
|
||||
List.delete(message["to"] || [], Pleroma.Constants.as_public()) ++
|
||||
[user.follower_address]
|
||||
{pubcnt, to} = delete_and_count(message["to"] || [], Pleroma.Constants.as_public())
|
||||
{flwcnt, cc} = delete_and_count(message["cc"] || [], user.follower_address)
|
||||
|
||||
cc =
|
||||
List.delete(message["cc"] || [], user.follower_address) ++
|
||||
[Pleroma.Constants.as_public()]
|
||||
cc = insert_if_needed(cc, pubcnt, Pleroma.Constants.as_public())
|
||||
to = insert_if_needed(to, flwcnt, user.follower_address)
|
||||
|
||||
message =
|
||||
message
|
||||
|
@ -65,8 +83,8 @@ defp check_delist(message, actions) do
|
|||
defp check_strip_followers(message, actions) do
|
||||
if :strip_followers in actions do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(message["actor"]) do
|
||||
to = List.delete(message["to"] || [], user.follower_address)
|
||||
cc = List.delete(message["cc"] || [], user.follower_address)
|
||||
{_, to} = delete_and_count(message["to"] || [], user.follower_address)
|
||||
{_, cc} = delete_and_count(message["cc"] || [], user.follower_address)
|
||||
|
||||
message =
|
||||
message
|
||||
|
|
|
@ -108,7 +108,7 @@ defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do
|
|||
end
|
||||
|
||||
defp intersection(list1, list2) do
|
||||
list1 -- list1 -- list2
|
||||
list1 -- (list1 -- list2)
|
||||
end
|
||||
|
||||
defp check_followers_only(%{host: actor_host} = _actor_info, object) do
|
||||
|
|
|
@ -15,6 +15,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.Object.Fetcher
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator
|
||||
|
@ -253,9 +254,28 @@ def fetch_actor(object) do
|
|||
end
|
||||
|
||||
def fetch_actor_and_object(object) do
|
||||
fetch_actor(object)
|
||||
Object.normalize(object["object"], fetch: true)
|
||||
:ok
|
||||
# Fetcher.fetch_object_from_id already first does a local db lookup
|
||||
with {:ok, %User{}} <- fetch_actor(object),
|
||||
{:ap_id, id} when is_binary(id) <-
|
||||
{:ap_id, Pleroma.Web.ActivityPub.Utils.get_ap_id(object["object"])},
|
||||
{:ok, %Object{}} <- Fetcher.fetch_object_from_id(id) do
|
||||
:ok
|
||||
else
|
||||
{:ap_id, id} ->
|
||||
{:error, {:validate, "Invalid AP id: #{inspect(id)}"}}
|
||||
|
||||
# if actor: late post from a previously unknown, deleted profile
|
||||
# if object: private post we're not allowed to access
|
||||
# (other HTTP replies might just indicate a temporary network failure though!)
|
||||
{:error, e} when e in [:not_found, :forbidden] ->
|
||||
{:error, :ignore}
|
||||
|
||||
{:error, _} = e ->
|
||||
e
|
||||
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
defp for_each_history_item(
|
||||
|
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator do
|
|||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
require Pleroma.Constants
|
||||
require Logger
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
|
@ -27,14 +28,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator do
|
|||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
{:ok, actor} = User.get_or_fetch_by_ap_id(data["actor"])
|
||||
with {_, {:ok, actor}} <- {:user, User.get_or_fetch_by_ap_id(data["actor"])},
|
||||
{_, {:ok, actor}} <- {:feataddr, maybe_refetch_user(actor)} do
|
||||
data
|
||||
|> maybe_fix_data_for_mastodon(actor)
|
||||
|> cast_data()
|
||||
|> validate_data(actor)
|
||||
else
|
||||
{:feataddr, _} ->
|
||||
{:error,
|
||||
{:validate,
|
||||
"Actor doesn't provide featured collection address to verify against: #{data["id"]}"}}
|
||||
|
||||
{:ok, actor} = maybe_refetch_user(actor)
|
||||
|
||||
data
|
||||
|> maybe_fix_data_for_mastodon(actor)
|
||||
|> cast_data()
|
||||
|> validate_data(actor)
|
||||
{:user, _} ->
|
||||
{:error, :link_resolve_failed}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_fix_data_for_mastodon(data, actor) do
|
||||
|
@ -73,6 +81,9 @@ defp maybe_refetch_user(%User{featured_address: address} = user) when is_binary(
|
|||
end
|
||||
|
||||
defp maybe_refetch_user(%User{ap_id: ap_id}) do
|
||||
Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
||||
# If the user didn't expose a featured collection before,
|
||||
# recheck now so we can verify perms for add/remove.
|
||||
# But wait at least 5s to avoid rapid refetches in edge cases
|
||||
User.get_or_fetch_by_ap_id(ap_id, maximum_age: 5)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -71,7 +71,7 @@ defp fix_tag(data), do: Map.drop(data, ["tag"])
|
|||
|
||||
defp fix_replies(%{"replies" => replies} = data) when is_list(replies), do: data
|
||||
|
||||
defp fix_replies(%{"replies" => %{"first" => first}} = data) do
|
||||
defp fix_replies(%{"replies" => %{"first" => first}} = data) when is_binary(first) do
|
||||
with {:ok, replies} <- Akkoma.Collections.Fetcher.fetch_collection(first) do
|
||||
Map.put(data, "replies", replies)
|
||||
else
|
||||
|
@ -81,6 +81,10 @@ defp fix_replies(%{"replies" => %{"first" => first}} = data) do
|
|||
end
|
||||
end
|
||||
|
||||
defp fix_replies(%{"replies" => %{"first" => %{"items" => replies}}} = data)
|
||||
when is_list(replies),
|
||||
do: Map.put(data, "replies", replies)
|
||||
|
||||
defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies),
|
||||
do: Map.put(data, "replies", replies)
|
||||
|
||||
|
|
|
@ -54,10 +54,14 @@ def validate_actor_presence(cng, options \\ []) do
|
|||
def validate_object_presence(cng, options \\ []) do
|
||||
field_name = Keyword.get(options, :field_name, :object)
|
||||
allowed_types = Keyword.get(options, :allowed_types, false)
|
||||
allowed_categories = Keyword.get(options, :allowed_object_categores, [:object, :activity])
|
||||
|
||||
cng
|
||||
|> validate_change(field_name, fn field_name, object_id ->
|
||||
object = Object.get_cached_by_ap_id(object_id) || Activity.get_by_ap_id(object_id)
|
||||
object =
|
||||
(:object in allowed_categories && Object.get_cached_by_ap_id(object_id)) ||
|
||||
(:activity in allowed_categories && Activity.get_by_ap_id(object_id)) ||
|
||||
nil
|
||||
|
||||
cond do
|
||||
!object ->
|
||||
|
|
|
@ -61,7 +61,10 @@ defp validate_data(cng) do
|
|||
|> validate_inclusion(:type, ["Delete"])
|
||||
|> validate_delete_actor(:actor)
|
||||
|> validate_modification_rights()
|
||||
|> validate_object_or_user_presence(allowed_types: @deletable_types)
|
||||
|> validate_object_or_user_presence(
|
||||
allowed_types: @deletable_types,
|
||||
allowed_object_categories: [:object]
|
||||
)
|
||||
|> add_deleted_activity_id()
|
||||
end
|
||||
|
||||
|
|
|
@ -129,7 +129,7 @@ defp validate_data(data_cng) do
|
|||
|> validate_inclusion(:type, ["EmojiReact"])
|
||||
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content])
|
||||
|> validate_actor_presence()
|
||||
|> validate_object_presence()
|
||||
|> validate_object_presence(allowed_object_categories: [:object])
|
||||
|> validate_emoji()
|
||||
|> maybe_validate_tag_presence()
|
||||
end
|
||||
|
|
|
@ -66,7 +66,7 @@ defp validate_data(data_cng) do
|
|||
|> validate_inclusion(:type, ["Like"])
|
||||
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc])
|
||||
|> validate_actor_presence()
|
||||
|> validate_object_presence()
|
||||
|> validate_object_presence(allowed_object_categories: [:object])
|
||||
|> validate_existing_like()
|
||||
end
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ defp validate_data(data_cng) do
|
|||
|> validate_inclusion(:type, ["Undo"])
|
||||
|> validate_required([:id, :type, :object, :actor, :to, :cc])
|
||||
|> validate_undo_actor(:actor)
|
||||
|> validate_object_presence()
|
||||
|> validate_object_presence(allowed_object_categories: [:activity])
|
||||
|> validate_undo_rights()
|
||||
end
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidator do
|
|||
@behaviour Pleroma.Web.ActivityPub.ObjectValidator.Validating
|
||||
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.Signature
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
|
@ -35,27 +34,18 @@ def validate(%{"type" => type, "id" => _id} = data, meta)
|
|||
|
||||
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
|
||||
defp validate_pubkey(%{
|
||||
"id" => user_id,
|
||||
"publicKey" => %{"id" => pk_id, "publicKeyPem" => _key}
|
||||
}) do
|
||||
with {_, true} <- {:user, is_binary(user_id)},
|
||||
{_, true} <- {:key, is_binary(pk_id)},
|
||||
:ok <- Containment.contain_key_user(pk_id, user_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"}
|
||||
{:user, _} -> {:error, "Invalid user id: #{inspect(user_id)}"}
|
||||
{:key, _} -> {:error, "Invalid key id: #{inspect(pk_id)}"}
|
||||
:error -> {:error, "Problematic actor-key pairing: #{user_id} - #{pk_id}"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ defp allowed_instances do
|
|||
Config.get([:mrf_simple, :accept])
|
||||
end
|
||||
|
||||
def should_federate?(url) do
|
||||
def should_federate?(url) when is_binary(url) do
|
||||
%{host: host} = URI.parse(url)
|
||||
|
||||
with {nil, false} <- {nil, is_nil(host)},
|
||||
|
@ -137,6 +137,8 @@ def should_federate?(url) do
|
|||
end
|
||||
end
|
||||
|
||||
def should_federate?(_), do: false
|
||||
|
||||
@spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
|
||||
defp recipients(actor, activity) do
|
||||
followers =
|
||||
|
@ -217,7 +219,6 @@ def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
|
|||
|
||||
inboxes =
|
||||
recipients
|
||||
|> Enum.filter(&User.ap_enabled?/1)
|
||||
|> Enum.map(fn actor -> actor.inbox end)
|
||||
|> Enum.filter(fn inbox -> should_federate?(inbox) end)
|
||||
|> Instances.filter_reachable()
|
||||
|
@ -259,7 +260,6 @@ def publish(%User{} = actor, %Activity{} = activity) do
|
|||
json = Jason.encode!(data)
|
||||
|
||||
recipients(actor, activity)
|
||||
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|
||||
|> Enum.map(fn %User{} = user ->
|
||||
determine_inbox(activity, user)
|
||||
end)
|
||||
|
|
|
@ -21,7 +21,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Workers.TransmogrifierWorker
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
|
@ -30,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
@doc """
|
||||
Modifies an incoming AP object (mastodon format) to our internal format.
|
||||
(This only deals with non-activity AP objects)
|
||||
"""
|
||||
def fix_object(object, options \\ []) do
|
||||
object
|
||||
|
@ -45,6 +45,38 @@ def fix_object(object, options \\ []) do
|
|||
|> fix_content_map()
|
||||
|> fix_addressing()
|
||||
|> fix_summary()
|
||||
|> fix_history(&fix_object/1)
|
||||
end
|
||||
|
||||
defp maybe_fix_object(%{"attributedTo" => _} = object), do: fix_object(object)
|
||||
defp maybe_fix_object(object), do: object
|
||||
|
||||
defp fix_history(%{"formerRepresentations" => %{"orderedItems" => list}} = obj, fix_fun)
|
||||
when is_list(list) do
|
||||
update_in(obj["formerRepresentations"]["orderedItems"], fn h -> Enum.map(h, fix_fun) end)
|
||||
end
|
||||
|
||||
defp fix_history(obj, _), do: obj
|
||||
|
||||
defp fix_recursive(obj, fun) do
|
||||
# unlike Erlang, Elixir does not support recursive inline functions
|
||||
# which would allow us to avoid reconstructing this on every recursion
|
||||
rec_fun = fn
|
||||
obj when is_map(obj) -> fix_recursive(obj, fun)
|
||||
# there may be simple AP IDs in history (or object field)
|
||||
obj -> obj
|
||||
end
|
||||
|
||||
obj
|
||||
|> fun.()
|
||||
|> fix_history(rec_fun)
|
||||
|> then(fn
|
||||
%{"object" => object} = doc when is_map(object) ->
|
||||
update_in(doc["object"], rec_fun)
|
||||
|
||||
apdoc ->
|
||||
apdoc
|
||||
end)
|
||||
end
|
||||
|
||||
def fix_summary(%{"summary" => nil} = object) do
|
||||
|
@ -415,17 +447,10 @@ defp get_reported(objects) do
|
|||
end
|
||||
|
||||
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)
|
||||
data
|
||||
|> fix_recursive(&normalise_addressing_public/1)
|
||||
|> fix_recursive(&strip_internal_fields/1)
|
||||
|> handle_incoming_normalised(options)
|
||||
end
|
||||
|
||||
defp handle_incoming_normalised(data, options)
|
||||
|
@ -491,7 +516,6 @@ defp handle_incoming_normalised(
|
|||
|
||||
object =
|
||||
data["object"]
|
||||
|> strip_internal_fields()
|
||||
|> fix_type(fetch_options)
|
||||
|> fix_in_reply_to(fetch_options)
|
||||
|> fix_quote_url(fetch_options)
|
||||
|
@ -520,10 +544,22 @@ defp handle_incoming_normalised(
|
|||
|
||||
defp handle_incoming_normalised(%{"type" => type} = data, _options)
|
||||
when type in ~w{Like EmojiReact Announce Add Remove} do
|
||||
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
||||
with {_, :ok} <- {:link, ObjectValidator.fetch_actor_and_object(data)},
|
||||
{:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:link, {:error, :ignore}} ->
|
||||
{:error, :ignore}
|
||||
|
||||
{:link, {:error, {:validate, _}} = e} ->
|
||||
e
|
||||
|
||||
{:link, {:error, {:reject, _}} = e} ->
|
||||
e
|
||||
|
||||
{:link, _} ->
|
||||
{:error, :link_resolve_failed}
|
||||
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
|
@ -534,6 +570,9 @@ defp handle_incoming_normalised(
|
|||
_options
|
||||
)
|
||||
when type in ~w{Update Block Follow Accept Reject} do
|
||||
fixed_obj = maybe_fix_object(data["object"])
|
||||
data = if fixed_obj != nil, do: %{data | "object" => fixed_obj}, else: data
|
||||
|
||||
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
|
||||
{:ok, activity, _} <-
|
||||
Pipeline.common_pipeline(data, local: false) do
|
||||
|
@ -545,22 +584,45 @@ defp handle_incoming_normalised(
|
|||
%{"type" => "Delete"} = data,
|
||||
_options
|
||||
) do
|
||||
with {:ok, activity, _} <-
|
||||
Pipeline.common_pipeline(data, local: false) do
|
||||
oid_result = ObjectValidators.ObjectID.cast(data["object"])
|
||||
|
||||
with {_, {:ok, object_id}} <- {:object_id, oid_result},
|
||||
object <- Object.get_cached_by_ap_id(object_id),
|
||||
{_, false} <- {:tombstone, Object.tombstone_object?(object) && !data["actor"]},
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, {:validate, _}} = e ->
|
||||
# Check if we have a create activity for this
|
||||
with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
|
||||
%Activity{data: %{"actor" => actor}} <-
|
||||
Activity.create_by_object_ap_id(object_id) |> Repo.one(),
|
||||
# We have one, insert a tombstone and retry
|
||||
{:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
|
||||
{:ok, _tombstone} <- Object.create(tombstone_data) do
|
||||
handle_incoming(data)
|
||||
{:object_id, _} ->
|
||||
{:error, {:validate, "Invalid object id: #{data["object"]}"}}
|
||||
|
||||
{:tombstone, true} ->
|
||||
{:error, :ignore}
|
||||
|
||||
{:error, {:validate, {:error, %Ecto.Changeset{errors: errors}}}} = e ->
|
||||
if errors[:object] == {"can't find object", []} do
|
||||
# Check if we have a create activity for this
|
||||
# (e.g. from a db prune without --prune-activities)
|
||||
# We'd still like to process side effects so insert a fake tombstone and retry
|
||||
# (real tombstones from Object.delete do not have an actor field)
|
||||
with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
|
||||
{_, %Activity{data: %{"actor" => actor}}} <-
|
||||
{:create, Activity.create_by_object_ap_id(object_id) |> Repo.one()},
|
||||
{:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
|
||||
{:ok, _tombstone} <- Object.create(tombstone_data) do
|
||||
handle_incoming(data)
|
||||
else
|
||||
{:create, _} -> {:error, :ignore}
|
||||
_ -> e
|
||||
end
|
||||
else
|
||||
_ -> e
|
||||
e
|
||||
end
|
||||
|
||||
{:error, _} = e ->
|
||||
e
|
||||
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -593,6 +655,20 @@ defp handle_incoming_normalised(
|
|||
when type in ["Like", "EmojiReact", "Announce", "Block"] do
|
||||
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, {:validate, {:error, %Ecto.Changeset{errors: errors}}}} = e ->
|
||||
# If we never saw the activity being undone, no need to do anything.
|
||||
# Inspectinging the validation error content is a bit akward, but looking up the Activity
|
||||
# ahead of time here would be too costly since Activity queries are not cached
|
||||
# and there's no way atm to pass the retrieved result along along
|
||||
if errors[:object] == {"can't find object", []} do
|
||||
{:error, :ignore}
|
||||
else
|
||||
e
|
||||
end
|
||||
|
||||
e ->
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -950,8 +1026,7 @@ defp build_emoji_tag({name, url}) do
|
|||
"icon" => %{"url" => "#{URI.encode(url)}", "type" => "Image"},
|
||||
"name" => ":" <> name <> ":",
|
||||
"type" => "Emoji",
|
||||
"updated" => "1970-01-01T00:00:00Z",
|
||||
"id" => url
|
||||
"updated" => "1970-01-01T00:00:00Z"
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -996,6 +1071,8 @@ def prepare_attachments(object) do
|
|||
Map.put(object, "attachment", attachments)
|
||||
end
|
||||
|
||||
# for outgoing docs immediately stripping internal fields recursively breaks later emoji transformations
|
||||
# (XXX: it would be better to reorder operations so we can always use recursive stripping)
|
||||
def strip_internal_fields(object) do
|
||||
Map.drop(object, Pleroma.Constants.object_internal_fields())
|
||||
end
|
||||
|
@ -1008,47 +1085,6 @@ defp strip_internal_tags(%{"tag" => tags} = object) do
|
|||
|
||||
defp strip_internal_tags(object), do: object
|
||||
|
||||
def perform(:user_upgrade, user) do
|
||||
# we pass a fake user so that the followers collection is stripped away
|
||||
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
|
||||
|
||||
from(
|
||||
a in Activity,
|
||||
where: ^old_follower_address in a.recipients,
|
||||
update: [
|
||||
set: [
|
||||
recipients:
|
||||
fragment(
|
||||
"array_replace(?,?,?)",
|
||||
a.recipients,
|
||||
^old_follower_address,
|
||||
^user.follower_address
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|> Repo.update_all([])
|
||||
end
|
||||
|
||||
def upgrade_user_from_ap_id(ap_id) do
|
||||
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
|
||||
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
|
||||
{:ok, user} <- update_user(user, data) do
|
||||
ActivityPub.enqueue_pin_fetches(user)
|
||||
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
|
||||
{:ok, user}
|
||||
else
|
||||
%User{} = user -> {:ok, user}
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
defp update_user(user, data) do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
|
||||
Map.put(data, "url", url["href"])
|
||||
end
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
defmodule Pleroma.Web.ActivityPub.UserView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
@ -33,9 +32,7 @@ def render("endpoints.json", %{user: %User{local: true} = _user}) do
|
|||
def render("endpoints.json", _), do: %{}
|
||||
|
||||
def render("service.json", %{user: user}) do
|
||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
{:ok, public_key} = User.SigningKey.public_key_pem(user)
|
||||
|
||||
endpoints = render("endpoints.json", %{user: user})
|
||||
|
||||
|
@ -52,7 +49,7 @@ def render("service.json", %{user: user}) do
|
|||
"url" => user.ap_id,
|
||||
"manuallyApprovesFollowers" => false,
|
||||
"publicKey" => %{
|
||||
"id" => "#{user.ap_id}#main-key",
|
||||
"id" => User.SigningKey.local_key_id(user.ap_id),
|
||||
"owner" => user.ap_id,
|
||||
"publicKeyPem" => public_key
|
||||
},
|
||||
|
@ -70,9 +67,12 @@ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
|
|||
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
|
||||
|
||||
def render("user.json", %{user: user}) do
|
||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
public_key =
|
||||
case User.SigningKey.public_key_pem(user) do
|
||||
{:ok, public_key} -> public_key
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
user = User.sanitize_html(user)
|
||||
|
||||
endpoints = render("endpoints.json", %{user: user})
|
||||
|
@ -97,7 +97,7 @@ def render("user.json", %{user: user}) do
|
|||
"url" => user.ap_id,
|
||||
"manuallyApprovesFollowers" => user.is_locked,
|
||||
"publicKey" => %{
|
||||
"id" => "#{user.ap_id}#main-key",
|
||||
"id" => User.SigningKey.local_key_id(user.ap_id),
|
||||
"owner" => user.ap_id,
|
||||
"publicKeyPem" => public_key
|
||||
},
|
||||
|
@ -116,6 +116,20 @@ def render("user.json", %{user: user}) do
|
|||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
def render("keys.json", %{user: user}) do
|
||||
{:ok, public_key} = User.SigningKey.public_key_pem(user)
|
||||
|
||||
%{
|
||||
"id" => user.ap_id,
|
||||
"publicKey" => %{
|
||||
"id" => User.SigningKey.key_id_of_local_user(user),
|
||||
"owner" => user.ap_id,
|
||||
"publicKeyPem" => public_key
|
||||
}
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
def render("following.json", %{user: user, page: page} = opts) do
|
||||
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
|
||||
showing_count = showing_items || !user.hide_follows_count
|
||||
|
|
|
@ -32,7 +32,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
|
|||
},
|
||||
voters_count: %Schema{
|
||||
type: :integer,
|
||||
description: "How many unique accounts have voted. Number."
|
||||
nullable: true,
|
||||
description:
|
||||
"How many unique accounts have voted for a multi-selection poll. Number, or null if single-selection poll."
|
||||
},
|
||||
voted: %Schema{
|
||||
type: :boolean,
|
||||
|
|
|
@ -66,10 +66,10 @@ defmodule Pleroma.Web.Endpoint do
|
|||
}
|
||||
)
|
||||
|
||||
plug(Plug.Static.IndexHtml, at: "/akkoma/swaggerui")
|
||||
plug(Plug.Static.IndexHtml, at: "/pleroma/swaggerui/")
|
||||
|
||||
plug(Pleroma.Web.Plugs.FrontendStatic,
|
||||
at: "/akkoma/swaggerui",
|
||||
at: "/pleroma/swaggerui",
|
||||
frontend_type: :swagger,
|
||||
gzip: true,
|
||||
if: &Pleroma.Web.Swagger.ui_enabled?/0,
|
||||
|
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.Web.Federator do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Federator.Publisher
|
||||
|
@ -92,10 +91,10 @@ def perform(:incoming_ap_doc, params) do
|
|||
|
||||
# NOTE: we use the actor ID to do the containment, this is fine because an
|
||||
# actor shouldn't be acting on objects outside their own AP server.
|
||||
with {_, {:ok, _user}} <- {:actor, ap_enabled_actor(actor)},
|
||||
nil <- Activity.normalize(params["id"]),
|
||||
with nil <- Activity.normalize(params["id"]),
|
||||
{_, :ok} <-
|
||||
{:correct_origin?, Containment.contain_origin_from_id(actor, params)},
|
||||
{_, :ok, _} <- {:local, Containment.contain_local_fetch(actor), actor},
|
||||
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
|
||||
{:ok, activity}
|
||||
else
|
||||
|
@ -103,6 +102,13 @@ def perform(:incoming_ap_doc, params) do
|
|||
Logger.debug("Origin containment failure for #{params["id"]}")
|
||||
{:error, :origin_containment_failed}
|
||||
|
||||
{:local, _, actor} ->
|
||||
Logger.alert(
|
||||
"Received incoming AP doc with valid signature for local actor #{actor}! Likely key leak!\n#{inspect(params)}"
|
||||
)
|
||||
|
||||
{:error, :origin_containment_failed}
|
||||
|
||||
%Activity{} ->
|
||||
Logger.debug("Already had #{params["id"]}")
|
||||
{:error, :already_present}
|
||||
|
@ -119,17 +125,11 @@ def perform(:incoming_ap_doc, params) do
|
|||
e ->
|
||||
# Just drop those for now
|
||||
Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end)
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def ap_enabled_actor(id) do
|
||||
user = User.get_cached_by_ap_id(id)
|
||||
|
||||
if User.ap_enabled?(user) do
|
||||
{:ok, user}
|
||||
else
|
||||
ActivityPub.make_user_from_ap_id(id)
|
||||
case e do
|
||||
{:error, _} -> e
|
||||
_ -> {:error, e}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,7 +19,7 @@ def render("show.json", %{object: object, multiple: multiple, options: options}
|
|||
expired: expired,
|
||||
multiple: multiple,
|
||||
votes_count: votes_count,
|
||||
voters_count: voters_count(object),
|
||||
voters_count: voters_count(multiple, object),
|
||||
options: options,
|
||||
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
|
||||
}
|
||||
|
@ -68,11 +68,19 @@ defp options_and_votes_count(options) do
|
|||
end)
|
||||
end
|
||||
|
||||
defp voters_count(%{data: %{"voters" => voters}}) when is_list(voters) do
|
||||
defp voters_count(false, _poll_data) do
|
||||
# Mastodon always sets voter count to "null" unless multiple options were selectable
|
||||
# Some clients may rely on this to detect multiple selection polls and it can mess
|
||||
# up percentages for some clients if we never got a correct remote voter count and
|
||||
# only count local voters here; see https://akkoma.dev/AkkomaGang/akkoma/issues/190
|
||||
nil
|
||||
end
|
||||
|
||||
defp voters_count(_multiple, %{data: %{"voters" => voters}}) when is_list(voters) do
|
||||
length(voters)
|
||||
end
|
||||
|
||||
defp voters_count(_), do: 0
|
||||
defp voters_count(_, _), do: 0
|
||||
|
||||
defp voted_and_own_votes(%{object: object} = params, options) do
|
||||
if params[:for] do
|
||||
|
|
|
@ -617,7 +617,9 @@ def render("context.json", %{activity: activity, activities: activities, user: u
|
|||
%{ancestors: ancestors, descendants: descendants} =
|
||||
activities
|
||||
|> Enum.reverse()
|
||||
|> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
|
||||
|> Enum.group_by(fn %{id: id} ->
|
||||
if id < activity.id, do: :ancestors, else: :descendants
|
||||
end)
|
||||
|> Map.put_new(:ancestors, [])
|
||||
|> Map.put_new(:descendants, [])
|
||||
|
||||
|
|
|
@ -52,11 +52,11 @@ def url(url) do
|
|||
|
||||
@spec url_proxiable?(String.t()) :: boolean()
|
||||
def url_proxiable?(url) do
|
||||
not local?(url) and not whitelisted?(url) and not blocked?(url)
|
||||
not local?(url) and not whitelisted?(url) and not blocked?(url) and http_scheme?(url)
|
||||
end
|
||||
|
||||
def preview_url(url, preview_params \\ []) do
|
||||
if preview_enabled?() do
|
||||
if preview_enabled?() and url_proxiable?(url) do
|
||||
encode_preview_url(url, preview_params)
|
||||
else
|
||||
url(url)
|
||||
|
@ -71,6 +71,8 @@ def preview_enabled?, do: enabled?() and !!Config.get([:media_preview_proxy, :en
|
|||
|
||||
def local?(url), do: String.starts_with?(url, Endpoint.url())
|
||||
|
||||
def http_scheme?(url), do: String.starts_with?(url, ["http:", "https:"])
|
||||
|
||||
def whitelisted?(url) do
|
||||
%{host: domain} = URI.parse(url)
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ def nodeinfo(conn, %{"version" => version}) when version in ["2.0", "2.1"] do
|
|||
conn
|
||||
|> put_resp_header(
|
||||
"content-type",
|
||||
"application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"
|
||||
"application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/#{version}#\"; charset=utf-8"
|
||||
)
|
||||
|> json(Nodeinfo.get_nodeinfo(version))
|
||||
end
|
||||
|
|
32
lib/pleroma/web/plugs/ensure_user_public_key_plug.ex
Normal file
32
lib/pleroma/web/plugs/ensure_user_public_key_plug.ex
Normal file
|
@ -0,0 +1,32 @@
|
|||
defmodule Pleroma.Web.Plugs.EnsureUserPublicKeyPlug do
|
||||
@moduledoc """
|
||||
This plug will attempt to pull in a user's public key if we do not have it.
|
||||
We _should_ be able to request the URL from the key URL...
|
||||
"""
|
||||
|
||||
alias Pleroma.User
|
||||
|
||||
def init(options), do: options
|
||||
|
||||
def call(conn, _opts) do
|
||||
key_id = key_id_from_conn(conn)
|
||||
|
||||
unless is_nil(key_id) do
|
||||
User.SigningKey.get_or_fetch_by_key_id(key_id)
|
||||
# now we SHOULD have the user that owns the key locally. maybe.
|
||||
# if we don't, we'll error out when we try to validate.
|
||||
end
|
||||
|
||||
conn
|
||||
end
|
||||
|
||||
defp key_id_from_conn(conn) do
|
||||
case HTTPSignatures.signature_for_conn(conn) do
|
||||
%{"keyId" => key_id} when is_binary(key_id) ->
|
||||
key_id
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,8 +8,8 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
|
|||
|
||||
use Pleroma.Web, :verified_routes
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Signature
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.User.SigningKey
|
||||
require Logger
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
@ -77,10 +77,6 @@ defp assign_valid_signature_on_route_aliases(conn, [path | rest]) do
|
|||
|> put_req_header("(request-target)", request_target)
|
||||
|> maybe_put_created_psudoheader()
|
||||
|> maybe_put_expires_psudoheader()
|
||||
|> case do
|
||||
%{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)
|
||||
conn -> conn
|
||||
end
|
||||
|
||||
conn
|
||||
|> assign(:valid_signature, HTTPSignatures.validate_conn(conn))
|
||||
|
@ -93,7 +89,13 @@ defp maybe_assign_valid_signature(conn) do
|
|||
# set (request-target) header to the appropriate value
|
||||
# we also replace the digest header with the one we computed
|
||||
possible_paths =
|
||||
route_aliases(conn) ++ [conn.request_path, conn.request_path <> "?#{conn.query_string}"]
|
||||
[conn.request_path, conn.request_path <> "?#{conn.query_string}" | route_aliases(conn)]
|
||||
|
||||
conn =
|
||||
case conn do
|
||||
%{assigns: %{digest: digest}} = conn -> put_req_header(conn, "digest", digest)
|
||||
conn -> conn
|
||||
end
|
||||
|
||||
assign_valid_signature_on_route_aliases(conn, possible_paths)
|
||||
else
|
||||
|
@ -139,12 +141,19 @@ defp maybe_require_signature(
|
|||
defp maybe_require_signature(conn), do: conn
|
||||
|
||||
defp signature_host(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, actor_id} <- Signature.key_id_to_actor_id(kid) do
|
||||
with {:key_id, %{"keyId" => kid}} <- {:key_id, HTTPSignatures.signature_for_conn(conn)},
|
||||
{:actor_id, actor_id, _} when actor_id != nil <-
|
||||
{:actor_id, SigningKey.key_id_to_ap_id(kid), kid} do
|
||||
actor_id
|
||||
else
|
||||
e ->
|
||||
{:error, e}
|
||||
{:key_id, e} ->
|
||||
Logger.error("Failed to extract key_id from signature: #{inspect(e)}")
|
||||
nil
|
||||
|
||||
{:actor_id, _, kid} ->
|
||||
# SigningKeys SHOULD have been fetched before this gets called!
|
||||
Logger.error("Failed to extract actor_id from signature: signing key #{kid} not known")
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -62,10 +62,10 @@ defp call_static(%{request_path: request_path} = conn, opts, from) do
|
|||
opts =
|
||||
opts
|
||||
|> Map.put(:from, from)
|
||||
|> Map.put(:set_content_type, false)
|
||||
|> Map.put(:content_types, false)
|
||||
|
||||
conn
|
||||
|> set_static_content_type(request_path)
|
||||
|> Pleroma.Web.Plugs.StaticNoCT.call(opts)
|
||||
|> Plug.Static.call(opts)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
|
||||
alias Pleroma.Helpers.AuthHelper
|
||||
alias Pleroma.Signature
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
||||
|
@ -33,7 +32,7 @@ def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = con
|
|||
|> assign(:valid_signature, false)
|
||||
|
||||
# remove me once testsuite uses mapped capabilities instead of what we do now
|
||||
{:user, nil} ->
|
||||
{:user, _} ->
|
||||
Logger.debug("Failed to map identity from signature (lookup failure)")
|
||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
|
||||
|
||||
|
@ -68,7 +67,8 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
|
|||
Logger.debug("Failed to map identity from signature (lookup failure)")
|
||||
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
|
||||
|
||||
only_permit_user_routes(conn)
|
||||
conn
|
||||
|> assign(:valid_signature, false)
|
||||
|
||||
_ ->
|
||||
Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
|
||||
|
@ -82,33 +82,34 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
|
|||
# no signature at all
|
||||
def call(conn, _opts), do: conn
|
||||
|
||||
defp only_permit_user_routes(%{path_info: ["users", _]} = conn) do
|
||||
conn
|
||||
|> assign(:limited_ap, true)
|
||||
end
|
||||
|
||||
defp only_permit_user_routes(conn) do
|
||||
conn
|
||||
|> assign(:valid_signature, false)
|
||||
end
|
||||
|
||||
defp key_id_from_conn(conn) do
|
||||
with %{"keyId" => key_id} <- HTTPSignatures.signature_for_conn(conn),
|
||||
{:ok, ap_id} <- Signature.key_id_to_actor_id(key_id) do
|
||||
ap_id
|
||||
else
|
||||
case HTTPSignatures.signature_for_conn(conn) do
|
||||
%{"keyId" => key_id} when is_binary(key_id) ->
|
||||
key_id
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp user_from_key_id(conn) do
|
||||
with key_actor_id when is_binary(key_actor_id) <- key_id_from_conn(conn),
|
||||
{:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(key_actor_id) do
|
||||
with {:key_id, key_id} when is_binary(key_id) <- {:key_id, key_id_from_conn(conn)},
|
||||
{:mapped_ap_id, ap_id} when is_binary(ap_id) <-
|
||||
{:mapped_ap_id, User.SigningKey.key_id_to_ap_id(key_id)},
|
||||
{:user_fetch, {:ok, %User{} = user}} <- {:user_fetch, User.get_or_fetch_by_ap_id(ap_id)} do
|
||||
user
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
{:key_id, nil} ->
|
||||
Logger.debug("Failed to map identity from signature (no key ID)")
|
||||
{:key_id, nil}
|
||||
|
||||
{:mapped_ap_id, nil} ->
|
||||
Logger.debug("Failed to map identity from signature (could not map key ID to AP ID)")
|
||||
{:mapped_ap_id, nil}
|
||||
|
||||
{:user_fetch, {:error, _}} ->
|
||||
Logger.debug("Failed to map identity from signature (lookup failure)")
|
||||
{:user_fetch, nil}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,469 +0,0 @@
|
|||
# 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
|
|
@ -88,12 +88,12 @@ defp get_media(conn, {:static_dir, directory}, opts) do
|
|||
Map.get(opts, :static_plug_opts)
|
||||
|> Map.put(:at, [@path])
|
||||
|> Map.put(:from, directory)
|
||||
|> Map.put(:set_content_type, false)
|
||||
|> Map.put(:content_types, false)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> set_content_type(opts, conn.request_path)
|
||||
|> Pleroma.Web.Plugs.StaticNoCT.call(static_opts)
|
||||
|> Plug.Static.call(static_opts)
|
||||
|
||||
if conn.halted do
|
||||
conn
|
||||
|
|
|
@ -57,6 +57,10 @@ def run(%{"url" => url, "url_hash" => url_hash} = args) do
|
|||
Logger.debug("Rich media error for #{url}: :content_type is #{type}")
|
||||
negative_cache(url_hash, :timer.minutes(30))
|
||||
|
||||
{:error, {:url, reason}} ->
|
||||
Logger.debug("Rich media error for #{url}: refusing URL #{inspect(reason)}")
|
||||
negative_cache(url_hash, :timer.minutes(180))
|
||||
|
||||
e ->
|
||||
Logger.debug("Rich media error for #{url}: #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
@ -82,7 +86,7 @@ defp maybe_schedule_expiration(url, fields) do
|
|||
end
|
||||
|
||||
defp stream_update(%{"activity_id" => activity_id}) do
|
||||
Logger.info("Rich media backfill: streaming update for activity #{activity_id}")
|
||||
Logger.debug("Rich media backfill: streaming update for activity #{activity_id}")
|
||||
|
||||
Pleroma.Activity.get_by_id(activity_id)
|
||||
|> Pleroma.Activity.normalize()
|
||||
|
|
|
@ -16,12 +16,13 @@ def parse(nil), do: nil
|
|||
@spec parse(String.t()) :: {:ok, map()} | {:error, any()}
|
||||
def parse(url) do
|
||||
with {_, true} <- {:config, @config_impl.get([:rich_media, :enabled])},
|
||||
:ok <- validate_page_url(url),
|
||||
{_, :ok} <- {:url, validate_page_url(url)},
|
||||
{:ok, data} <- parse_url(url) do
|
||||
data = Map.put(data, "url", url)
|
||||
{:ok, data}
|
||||
else
|
||||
{:config, _} -> {:error, :rich_media_disabled}
|
||||
{:url, {:error, reason}} -> {:error, {:url, reason}}
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
@ -62,7 +63,7 @@ defp clean_parsed_data(data) do
|
|||
|> Map.new()
|
||||
end
|
||||
|
||||
@spec validate_page_url(URI.t() | binary()) :: :ok | :error
|
||||
@spec validate_page_url(URI.t() | binary()) :: :ok | {:error, term()}
|
||||
defp validate_page_url(page_url) when is_binary(page_url) do
|
||||
validate_tld = @config_impl.get([Pleroma.Formatter, :validate_tld])
|
||||
|
||||
|
@ -74,20 +75,20 @@ defp validate_page_url(page_url) when is_binary(page_url) do
|
|||
defp validate_page_url(%URI{host: host, scheme: "https"}) do
|
||||
cond do
|
||||
Linkify.Parser.ip?(host) ->
|
||||
:error
|
||||
{:error, :ip}
|
||||
|
||||
host in @config_impl.get([:rich_media, :ignore_hosts], []) ->
|
||||
:error
|
||||
{:error, :ignore_hosts}
|
||||
|
||||
get_tld(host) in @config_impl.get([:rich_media, :ignore_tld], []) ->
|
||||
:error
|
||||
{:error, :ignore_tld}
|
||||
|
||||
true ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_page_url(_), do: :error
|
||||
defp validate_page_url(_), do: {:error, "scheme mismatch"}
|
||||
|
||||
defp parse_uri(true, url) do
|
||||
url
|
||||
|
@ -95,7 +96,7 @@ defp parse_uri(true, url) do
|
|||
|> validate_page_url
|
||||
end
|
||||
|
||||
defp parse_uri(_, _), do: :error
|
||||
defp parse_uri(_, _), do: {:error, "not a URL"}
|
||||
|
||||
defp get_tld(host) do
|
||||
host
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Web.Router do
|
||||
use Pleroma.Web, :router
|
||||
import Phoenix.LiveDashboard.Router
|
||||
import Oban.Web.Router
|
||||
|
||||
pipeline :accepts_html do
|
||||
plug(:accepts, ["html"])
|
||||
|
@ -144,7 +145,14 @@ defmodule Pleroma.Web.Router do
|
|||
})
|
||||
end
|
||||
|
||||
pipeline :optional_http_signature do
|
||||
plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug)
|
||||
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
|
||||
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
|
||||
end
|
||||
|
||||
pipeline :http_signature do
|
||||
plug(Pleroma.Web.Plugs.EnsureUserPublicKeyPlug)
|
||||
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
|
||||
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
|
||||
plug(Pleroma.Web.Plugs.EnsureHTTPSignaturePlug)
|
||||
|
@ -745,7 +753,7 @@ defmodule Pleroma.Web.Router do
|
|||
scope "/", Pleroma.Web do
|
||||
# Note: html format is supported only if static FE is enabled
|
||||
# Note: http signature is only considered for json requests (no auth for non-json requests)
|
||||
pipe_through([:accepts_html_xml_json, :http_signature, :static_fe])
|
||||
pipe_through([:accepts_html_xml_json, :optional_http_signature, :static_fe])
|
||||
|
||||
# Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
|
||||
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
|
||||
|
@ -801,9 +809,6 @@ defmodule Pleroma.Web.Router do
|
|||
pipe_through([:activitypub_client])
|
||||
|
||||
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
|
||||
|
||||
get("/users/:nickname/outbox", ActivityPubController, :outbox)
|
||||
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web.ActivityPub do
|
||||
|
@ -818,7 +823,9 @@ defmodule Pleroma.Web.Router do
|
|||
scope "/", Pleroma.Web.ActivityPub do
|
||||
pipe_through(:activitypub)
|
||||
post("/inbox", ActivityPubController, :inbox)
|
||||
get("/users/:nickname/outbox", ActivityPubController, :outbox)
|
||||
post("/users/:nickname/inbox", ActivityPubController, :inbox)
|
||||
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
|
||||
end
|
||||
|
||||
scope "/relay", Pleroma.Web.ActivityPub do
|
||||
|
@ -896,6 +903,8 @@ defmodule Pleroma.Web.Router do
|
|||
metrics: {Pleroma.Web.Telemetry, :live_dashboard_metrics},
|
||||
csp_nonce_assign_key: :csp_nonce
|
||||
)
|
||||
|
||||
oban_dashboard("/akkoma/oban", csp_nonce_assign_key: :csp_nonce)
|
||||
end
|
||||
|
||||
# Test-only routes needed to test action dispatching and plug chain execution
|
||||
|
|
|
@ -208,8 +208,10 @@ defp summary_fallback_metrics(byte_unit \\ :byte) do
|
|||
dist_metrics ++ vm_metrics
|
||||
end
|
||||
|
||||
defp common_metrics do
|
||||
defp common_metrics(byte_unit \\ :byte) do
|
||||
[
|
||||
last_value("vm.portio.in.total", unit: {:byte, byte_unit}),
|
||||
last_value("vm.portio.out.total", unit: {:byte, byte_unit}),
|
||||
last_value("pleroma.local_users.total"),
|
||||
last_value("pleroma.domains.total"),
|
||||
last_value("pleroma.local_statuses.total"),
|
||||
|
@ -220,14 +222,22 @@ defp common_metrics do
|
|||
def prometheus_metrics,
|
||||
do: common_metrics() ++ distribution_metrics() ++ summary_fallback_metrics()
|
||||
|
||||
def live_dashboard_metrics, do: common_metrics() ++ summary_metrics(:megabyte)
|
||||
def live_dashboard_metrics, do: common_metrics(:megabyte) ++ summary_metrics(:megabyte)
|
||||
|
||||
defp periodic_measurements do
|
||||
[
|
||||
{__MODULE__, :io_stats, []},
|
||||
{__MODULE__, :instance_stats, []}
|
||||
]
|
||||
end
|
||||
|
||||
def io_stats do
|
||||
# All IO done via erlang ports, i.e. mostly network but also e.g. fasthtml_workers. NOT disk IO!
|
||||
{{:input, input}, {:output, output}} = :erlang.statistics(:io)
|
||||
:telemetry.execute([:vm, :portio, :in], %{total: input}, %{})
|
||||
:telemetry.execute([:vm, :portio, :out], %{total: output}, %{})
|
||||
end
|
||||
|
||||
def instance_stats do
|
||||
stats = Stats.get_stats()
|
||||
:telemetry.execute([:pleroma, :local_users], %{total: stats.user_count}, %{})
|
||||
|
|
|
@ -11,7 +11,4 @@
|
|||
<%= if User.banner_url(@user) do %>
|
||||
<link rel="header" href="<%= User.banner_url(@user) %>"/>
|
||||
<% end %>
|
||||
<%= if @user.local do %>
|
||||
<ap_enabled>true</ap_enabled>
|
||||
<% end %>
|
||||
</author>
|
||||
|
|
|
@ -11,7 +11,4 @@
|
|||
<%= if User.banner_url(@user) do %>
|
||||
<link rel="header"><%= User.banner_url(@user) %></link>
|
||||
<% end %>
|
||||
<%= if @user.local do %>
|
||||
<ap_enabled>true</ap_enabled>
|
||||
<% end %>
|
||||
</managingEditor>
|
||||
|
|
|
@ -8,9 +8,6 @@
|
|||
<%= if User.banner_url(@actor) do %>
|
||||
<link rel="header" href="<%= User.banner_url(@actor) %>"/>
|
||||
<% end %>
|
||||
<%= if @actor.local do %>
|
||||
<ap_enabled>true</ap_enabled>
|
||||
<% end %>
|
||||
|
||||
<poco:preferredUsername><%= @actor.nickname %></poco:preferredUsername>
|
||||
<poco:displayName><%= @actor.name %></poco:displayName>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<title>
|
||||
<%= Config.get([:instance, :name]) %>
|
||||
{Config.get([:instance, :name])}
|
||||
</title>
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="manifest" type="applicaton/manifest+json" {%{href: ~p"/web/manifest.json"}} />
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<title>
|
||||
<%= Config.get([:instance, :name]) %>
|
||||
{Config.get([:instance, :name])}
|
||||
</title>
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="manifest" type="applicaton/manifest+json" {%{href: ~p"/web/manifest.json"}} />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<a class="attachment" href="<%= @url %>" alt=<%= @name %>" title="<%= @name %>">
|
||||
<a class="attachment" href="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
|
||||
<%= if @nsfw do %>
|
||||
<div class="nsfw-banner">
|
||||
<div><%= gettext("Hover to show content") %></div>
|
||||
|
|
|
@ -5,30 +5,65 @@
|
|||
defmodule Pleroma.Workers.AttachmentsCleanupWorker do
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
||||
use Pleroma.Workers.WorkerHelper, queue: "attachments_cleanup"
|
||||
|
||||
@doc """
|
||||
Takes object data and if necessary enqueues a job,
|
||||
deleting all attachments of the post eligible for cleanup
|
||||
"""
|
||||
@spec enqueue_if_needed(map()) :: {:ok, Oban.Job.t()} | {:ok, :skip} | {:error, any()}
|
||||
def enqueue_if_needed(%{
|
||||
"actor" => actor,
|
||||
"attachment" => [_ | _] = attachments
|
||||
}) do
|
||||
with true <- Config.get([:instance, :cleanup_attachments]),
|
||||
true <- URI.parse(actor).host == Pleroma.Web.Endpoint.host(),
|
||||
[_ | _] <- attachments do
|
||||
enqueue(
|
||||
"cleanup_attachments",
|
||||
%{"actor" => actor, "attachments" => attachments},
|
||||
schedule_in: Config.get!([:instance, :cleanup_attachments_delay])
|
||||
)
|
||||
else
|
||||
_ -> {:ok, :skip}
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue_if_needed(_), do: {:ok, :skip}
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{
|
||||
args: %{
|
||||
"op" => "cleanup_attachments",
|
||||
"object" => %{"data" => %{"attachment" => [_ | _] = attachments, "actor" => actor}}
|
||||
"attachments" => [_ | _] = attachments,
|
||||
"actor" => actor
|
||||
}
|
||||
}) do
|
||||
if Pleroma.Config.get([:instance, :cleanup_attachments], false) do
|
||||
attachments
|
||||
|> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end)
|
||||
|> fetch_objects
|
||||
|> prepare_objects(actor, Enum.map(attachments, & &1["name"]))
|
||||
|> filter_objects
|
||||
|> do_clean
|
||||
end
|
||||
attachments
|
||||
|> Enum.flat_map(fn item -> Enum.map(item["url"], & &1["href"]) end)
|
||||
|> fetch_objects
|
||||
|> prepare_objects(actor, Enum.map(attachments, & &1["name"]))
|
||||
|> filter_objects
|
||||
|> do_clean
|
||||
|
||||
{:ok, :success}
|
||||
end
|
||||
|
||||
# Left over already enqueued jobs in the old format
|
||||
# This function clause can be deleted once sufficient time passed after 3.14
|
||||
def perform(%Job{
|
||||
args: %{
|
||||
"op" => "cleanup_attachments",
|
||||
"object" => %{"data" => data}
|
||||
}
|
||||
}) do
|
||||
enqueue_if_needed(data)
|
||||
end
|
||||
|
||||
def perform(%Job{args: %{"op" => "cleanup_attachments", "object" => _object}}), do: {:ok, :skip}
|
||||
|
||||
defp do_clean({object_ids, attachment_urls}) do
|
||||
|
|
|
@ -6,7 +6,9 @@ defmodule Pleroma.Workers.MailerWorker do
|
|||
use Pleroma.Workers.WorkerHelper, queue: "mailer"
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{args: %{"op" => "email", "encoded_email" => encoded_email, "config" => config}}) do
|
||||
def perform(%Job{
|
||||
args: %{"op" => "email", "encoded_email" => encoded_email, "config" => config}
|
||||
}) do
|
||||
encoded_email
|
||||
|> Base.decode64!()
|
||||
|> :erlang.binary_to_term()
|
||||
|
|
|
@ -6,7 +6,9 @@ defmodule Pleroma.Workers.MuteExpireWorker do
|
|||
use Pleroma.Workers.WorkerHelper, queue: "mute_expire"
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}}) do
|
||||
def perform(%Job{
|
||||
args: %{"op" => "unmute_user", "muter_id" => muter_id, "mutee_id" => mutee_id}
|
||||
}) do
|
||||
Pleroma.User.unmute(muter_id, mutee_id)
|
||||
:ok
|
||||
end
|
||||
|
|
|
@ -1,9 +1,30 @@
|
|||
defmodule Pleroma.Workers.NodeInfoFetcherWorker do
|
||||
use Pleroma.Workers.WorkerHelper, queue: "nodeinfo_fetcher"
|
||||
use Pleroma.Workers.WorkerHelper,
|
||||
queue: "nodeinfo_fetcher",
|
||||
unique: [
|
||||
keys: [:op, :source_url],
|
||||
# old jobs still get pruned after a short while
|
||||
period: :infinity,
|
||||
states: Oban.Job.states()
|
||||
]
|
||||
|
||||
alias Oban.Job
|
||||
alias Pleroma.Instances.Instance
|
||||
|
||||
def enqueue(op, %{"source_url" => ap_id} = params, worker_args) do
|
||||
# reduce to base url to avoid enqueueing unneccessary duplicates
|
||||
domain =
|
||||
ap_id
|
||||
|> URI.parse()
|
||||
|> URI.merge("/")
|
||||
|
||||
if Instance.needs_update(domain) do
|
||||
do_enqueue(op, %{params | "source_url" => URI.to_string(domain)}, worker_args)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{
|
||||
args: %{"op" => "process", "source_url" => domain}
|
||||
|
|
|
@ -13,7 +13,9 @@ def backoff(%Job{attempt: attempt}) when is_integer(attempt) do
|
|||
end
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{args: %{"op" => "publish", "activity_id" => activity_id, "object_data" => nil}}) do
|
||||
def perform(%Job{
|
||||
args: %{"op" => "publish", "activity_id" => activity_id, "object_data" => nil}
|
||||
}) do
|
||||
activity = Activity.get_by_id(activity_id)
|
||||
Federator.perform(:publish, activity)
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Workers.ReceiverWorker do
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Web.Federator
|
||||
|
||||
use Pleroma.Workers.WorkerHelper, queue: "federator_incoming"
|
||||
|
@ -12,10 +14,49 @@ def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do
|
|||
with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
|
||||
{:ok, res}
|
||||
else
|
||||
{:error, :origin_containment_failed} -> {:discard, :origin_containment_failed}
|
||||
{:error, {:reject, reason}} -> {:discard, reason}
|
||||
{:error, _} = e -> e
|
||||
e -> {:error, e}
|
||||
{:error, :origin_containment_failed} ->
|
||||
{:discard, :origin_containment_failed}
|
||||
|
||||
{:error, {:reject, reason}} ->
|
||||
{:discard, reason}
|
||||
|
||||
{:error, :already_present} ->
|
||||
{:discard, :already_present}
|
||||
|
||||
{:error, :ignore} ->
|
||||
{:discard, :ignore}
|
||||
|
||||
# invalid data or e.g. deleting an object we don't know about anyway
|
||||
{:error, {:validate, issue}} ->
|
||||
Logger.info("Received invalid AP document: #{inspect(issue)}")
|
||||
{:discard, :invalid}
|
||||
|
||||
# rarer, but sometimes there’s an additional :error in front
|
||||
{:error, {:error, {:validate, issue}}} ->
|
||||
Logger.info("Received invalid AP document: (2e) #{inspect(issue)}")
|
||||
{:discard, :invalid}
|
||||
|
||||
# failed to resolve a necessary referenced remote AP object;
|
||||
# might be temporary server/network trouble thus reattempt
|
||||
{:error, :link_resolve_failed} = e ->
|
||||
Logger.info("Failed to resolve AP link; may retry: #{inspect(params)}")
|
||||
e
|
||||
|
||||
{:error, _} = e ->
|
||||
Logger.error("Unexpected AP doc error: #{inspect(e)} from #{inspect(params)}")
|
||||
e
|
||||
|
||||
e ->
|
||||
Logger.error("Unexpected AP doc error: (raw) #{inspect(e)} from #{inspect(params)}")
|
||||
{:error, e}
|
||||
end
|
||||
rescue
|
||||
err ->
|
||||
Logger.error(
|
||||
"Receiver worker CRASH on #{inspect(params)} with: #{Exception.format(:error, err, __STACKTRACE__)}"
|
||||
)
|
||||
|
||||
# reraise to let oban handle transaction conflicts without deductig an attempt
|
||||
reraise err, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,23 +1,38 @@
|
|||
defmodule Pleroma.Workers.SearchIndexingWorker do
|
||||
use Pleroma.Workers.WorkerHelper, queue: "search_indexing"
|
||||
|
||||
@impl Oban.Worker
|
||||
defp search_module(), do: Pleroma.Config.get!([Pleroma.Search, :module])
|
||||
|
||||
def enqueue("add_to_index", params, worker_args) do
|
||||
if Kernel.function_exported?(search_module(), :add_to_index, 1) do
|
||||
do_enqueue("add_to_index", params, worker_args)
|
||||
else
|
||||
# XXX: or {:ok, nil} to more closely match Oban.inset()'s {:ok, job}?
|
||||
# or similar to unique coflict: %Oban.Job{conflict?: true} (but omitting all other fileds...)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue("remove_from_index", params, worker_args) do
|
||||
if Kernel.function_exported?(search_module(), :remove_from_index, 1) do
|
||||
do_enqueue("remove_from_index", params, worker_args)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{args: %{"op" => "add_to_index", "activity" => activity_id}}) do
|
||||
activity = Pleroma.Activity.get_by_id_with_object(activity_id)
|
||||
|
||||
search_module = Pleroma.Config.get([Pleroma.Search, :module])
|
||||
|
||||
search_module.add_to_index(activity)
|
||||
search_module().add_to_index(activity)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def perform(%Job{args: %{"op" => "remove_from_index", "object" => object_id}}) do
|
||||
search_module = Pleroma.Config.get([Pleroma.Search, :module])
|
||||
|
||||
# Fake the object so we can remove it from the index without having to keep it in the DB
|
||||
search_module.remove_from_index(%Pleroma.Object{id: object_id})
|
||||
search_module().remove_from_index(%Pleroma.Object{id: object_id})
|
||||
|
||||
:ok
|
||||
end
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Workers.TransmogrifierWorker do
|
||||
alias Pleroma.User
|
||||
|
||||
use Pleroma.Workers.WorkerHelper, queue: "transmogrifier"
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{args: %{"op" => "user_upgrade", "user_id" => user_id}}) do
|
||||
user = User.get_cached_by_id(user_id)
|
||||
Pleroma.Web.ActivityPub.Transmogrifier.perform(:user_upgrade, user)
|
||||
end
|
||||
end
|
|
@ -38,7 +38,7 @@ defmacro __using__(opts) do
|
|||
|
||||
alias Oban.Job
|
||||
|
||||
def enqueue(op, params, worker_args \\ []) do
|
||||
defp do_enqueue(op, params, worker_args \\ []) do
|
||||
params = Map.merge(%{"op" => op}, params)
|
||||
queue_atom = String.to_atom(unquote(queue))
|
||||
worker_args = worker_args ++ WorkerHelper.worker_args(queue_atom)
|
||||
|
@ -48,11 +48,16 @@ def enqueue(op, params, worker_args \\ []) do
|
|||
|> Oban.insert()
|
||||
end
|
||||
|
||||
def enqueue(op, params, worker_args \\ []),
|
||||
do: do_enqueue(op, params, worker_args)
|
||||
|
||||
@impl Oban.Worker
|
||||
def timeout(_job) do
|
||||
queue_atom = String.to_atom(unquote(queue))
|
||||
Config.get([:workers, :timeout, queue_atom], :timer.minutes(1))
|
||||
end
|
||||
|
||||
defoverridable enqueue: 3
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
30
mix.exs
30
mix.exs
|
@ -4,8 +4,8 @@ defmodule Pleroma.Mixfile do
|
|||
def project do
|
||||
[
|
||||
app: :pleroma,
|
||||
version: version("3.13.2"),
|
||||
elixir: "~> 1.14",
|
||||
version: version("3.15.2"),
|
||||
elixir: "~> 1.14.1 or ~> 1.15",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
compilers: Mix.compilers(),
|
||||
elixirc_options: [warnings_as_errors: warnings_as_errors()],
|
||||
|
@ -14,7 +14,7 @@ def project do
|
|||
aliases: aliases(),
|
||||
deps: deps(),
|
||||
test_coverage: [tool: ExCoveralls],
|
||||
preferred_cli_env: ["coveralls.html": :test],
|
||||
preferred_cli_env: ["coveralls.html": :test, "mneme.test": :test, "mneme.watch": :test],
|
||||
# Docs
|
||||
name: "Akkoma",
|
||||
homepage_url: "https://akkoma.dev/",
|
||||
|
@ -117,16 +117,17 @@ defp deps do
|
|||
[
|
||||
{:phoenix, "~> 1.7.0"},
|
||||
{:phoenix_view, "~> 2.0"},
|
||||
{:phoenix_live_dashboard, "~> 0.7.2"},
|
||||
{:phoenix_live_dashboard, "~> 0.8.6"},
|
||||
{:tzdata, "~> 1.1.1"},
|
||||
{:plug_cowboy, "~> 2.6"},
|
||||
{:phoenix_pubsub, "~> 2.1"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
{:phoenix_ecto, "~> 4.6"},
|
||||
{:inet_cidr, "~> 1.0.0"},
|
||||
{:ecto_enum, "~> 1.4"},
|
||||
{:ecto_sql, "~> 3.10.0"},
|
||||
{:postgrex, "~> 0.17.2"},
|
||||
{:oban, "~> 2.17.8"},
|
||||
{:ecto_sql, "~> 3.12.0"},
|
||||
{:postgrex, "~> 0.20.0"},
|
||||
{:oban, "~> 2.19.0"},
|
||||
{:oban_web, "~> 2.11.0"},
|
||||
{:gettext, "~> 0.22.3"},
|
||||
{:bcrypt_elixir, "~> 3.0.1"},
|
||||
{:fast_sanitize, "~> 0.2.3"},
|
||||
|
@ -144,7 +145,7 @@ defp deps do
|
|||
{:ex_aws, "~> 2.4"},
|
||||
{:ex_aws_s3, "~> 2.4"},
|
||||
{:sweet_xml, "~> 0.7"},
|
||||
{:earmark, "~> 1.4"},
|
||||
{:earmark, "1.4.46"},
|
||||
{:bbcode_pleroma, "~> 0.2.0"},
|
||||
{:argon2_elixir, "~> 3.1"},
|
||||
{:cors_plug, "~> 3.0"},
|
||||
|
@ -180,7 +181,7 @@ defp deps do
|
|||
{:remote_ip, "~> 1.1.0"},
|
||||
{:captcha,
|
||||
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
|
||||
ref: "90f6ce7672f70f56708792a98d98bd05176c9176"},
|
||||
ref: "6630c42aaaab124e697b4e513190c89d8b64e410"},
|
||||
{:restarter, path: "./restarter"},
|
||||
{:majic,
|
||||
git: "https://akkoma.dev/AkkomaGang/majic.git",
|
||||
|
@ -191,16 +192,16 @@ defp deps do
|
|||
git: "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git",
|
||||
ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"},
|
||||
{:nimble_parsec, "~> 1.3", override: true},
|
||||
{:ecto_psql_extras, "~> 0.7"},
|
||||
{:ecto_psql_extras, "~> 0.8"},
|
||||
{:elasticsearch,
|
||||
git: "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", ref: "main"},
|
||||
{:mfm_parser,
|
||||
git: "https://akkoma.dev/AkkomaGang/mfm-parser.git",
|
||||
ref: "b21ab7754024af096f2d14247574f55f0063295b"},
|
||||
ref: "360a30267a847810a63ab48f606ba227b2ca05f0"},
|
||||
|
||||
## dev & test
|
||||
{:ex_doc, "~> 0.30", only: :dev, runtime: false},
|
||||
{:ex_machina, "~> 2.7", only: :test},
|
||||
{:ex_machina, "~> 2.8", only: :test},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:mock, "~> 0.3.8", only: :test},
|
||||
{:excoveralls, "0.16.1", only: :test},
|
||||
|
@ -209,7 +210,8 @@ defp deps do
|
|||
{:dialyxir, "~> 1.3", only: [:dev], runtime: false},
|
||||
{:elixir_xml_to_map, "~> 3.0", only: :test},
|
||||
{:mint, "~> 1.5.1", override: true},
|
||||
{:nimble_pool, "~> 1.0", override: true}
|
||||
{:nimble_pool, "~> 1.0", override: true},
|
||||
{:mneme, "~> 0.10.2", only: [:dev, :test]}
|
||||
] ++ oauth_deps()
|
||||
end
|
||||
|
||||
|
|
119
mix.lock
119
mix.lock
|
@ -7,129 +7,142 @@
|
|||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},
|
||||
"calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.1.201603 or ~> 0.5.20 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"},
|
||||
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "90f6ce7672f70f56708792a98d98bd05176c9176", [ref: "90f6ce7672f70f56708792a98d98bd05176c9176"]},
|
||||
"castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
|
||||
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
||||
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "6630c42aaaab124e697b4e513190c89d8b64e410", [ref: "6630c42aaaab124e697b4e513190c89d8b64e410"]},
|
||||
"castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
|
||||
"certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
|
||||
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
||||
"concurrent_limiter": {:git, "https://akkoma.dev/AkkomaGang/concurrent-limiter.git", "a9e0b3d64574bdba761f429bb4fba0cf687b3338", [ref: "a9e0b3d64574bdba761f429bb4fba0cf687b3338"]},
|
||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||
"cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
|
||||
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
|
||||
"cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
|
||||
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
|
||||
"cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"},
|
||||
"credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"},
|
||||
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
|
||||
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
|
||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
|
||||
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"},
|
||||
"eblurhash": {:hex, :eblurhash, "1.2.2", "7da4255aaea984b31bb71155f673257353b0e0554d0d30dcf859547e74602582", [:rebar3], [], "hexpm", "8c20ca00904de023a835a9dcb7b7762fed32264c85a80c3cafa85288e405044c"},
|
||||
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
|
||||
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
|
||||
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
|
||||
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.0", "440719cd74f09b3f01c84455707a2c3972b725c513808e68eb6c5b0ab82bf523", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 0.18.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "f1512812dc196bcb932a96c82e55f69b543dc125e9d39f5e3631a9c4ec65ef12"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},
|
||||
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.7", "49943fe6bce07281fe3adfc2a23d3794e2acc644dfe98411cb5712ffecb6ad1a", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "ac0a0bce57ffe36b30fac2a2d0d427b04de016e6af5db6f4b41afa1241f39cda"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
|
||||
"elasticsearch": {:git, "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", "6cd946f75f6ab9042521a009d1d32d29a90113ca", [ref: "main"]},
|
||||
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||
"elixir_xml_to_map": {:hex, :elixir_xml_to_map, "3.1.0", "4d6260486a8cce59e4bf3575fe2dd2a24766546ceeef9f93fcec6f7c62a2827a", [:mix], [{:erlsom, "~> 1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm", "8fe5f2e75f90bab07ee2161120c2dc038ebcae8135554f5582990f1c8c21f911"},
|
||||
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
||||
"erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"},
|
||||
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
|
||||
"erlsom": {:hex, :erlsom, "1.5.2", "3e47c53a199136fb4d20d5479edb7c9229f31624534c062633951c8d14dcd276", [:rebar3], [], "hexpm", "4e765cc677fb30509f7b628ff2914e124cf4dcc0fac1c0a62ee4dcee24215b5d"},
|
||||
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
|
||||
"ex_aws": {:hex, :ex_aws, "2.5.4", "86c5bb870a49e0ab6f5aa5dd58cf505f09d2624ebe17530db3c1b61c88a673af", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e82bd0091bb9a5bb190139599f922ff3fc7aebcca4374d65c99c4e23aa6d1625"},
|
||||
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"},
|
||||
"ex_aws": {:hex, :ex_aws, "2.5.8", "0393cfbc5e4a9e7017845451a015d836a670397100aa4c86901980e2a2c5f7d4", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f79777b7932168956c8cc3a6db41f5783aa816eb50de356aed3165a71e5f8c3"},
|
||||
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.6", "d135983bbd8b6df6350dfd83999437725527c1bea151e5055760bfc9b2d17c20", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "9874e12847e469ca2f13a5689be04e546c16f63caf6380870b7f25bf7cb98875"},
|
||||
"ex_const": {:hex, :ex_const, "0.3.0", "9d79516679991baf540ef445438eef1455ca91cf1a3c2680d8fb9e5bea2fe4de", [:mix], [], "hexpm", "76546322abb9e40ee4a2f454cf1c8a5b25c3672fa79bed1ea52c31e0d2428ca9"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"},
|
||||
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.37.2", "2a3aa7014094f0e4e286a82aa5194a34dd17057160988b8509b15aa6c292720c", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "4dfa56075ce4887e4e8b1dcc121cd5fcb0f02b00391fd367ff5336d98fa49049"},
|
||||
"ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"},
|
||||
"ex_syslogger": {:hex, :ex_syslogger, "2.0.0", "de6de5c5472a9c4fdafb28fa6610e381ae79ebc17da6490b81d785d68bd124c9", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "a52b2fe71764e9e6ecd149ab66635812f68e39279cbeee27c52c0e35e8b8019e"},
|
||||
"excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"},
|
||||
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
|
||||
"fast_html": {:hex, :fast_html, "2.3.0", "08c1d8ead840dd3060ba02c761bed9f37f456a1ddfe30bcdcfee8f651cec06a6", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f18e3c7668f82d3ae0b15f48d48feeb257e28aa5ab1b0dbf781c7312e5da029d"},
|
||||
"fast_html": {:hex, :fast_html, "2.4.1", "73142526cee294b0ec8cf122483f32c861e4a9d988c60cd04f63ce7fd9e5a620", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "767a63ecc941d3fc0e0e9609ded1a5e798398e5b1bf4d2f47bcb5992a86b32cf"},
|
||||
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
|
||||
"file_ex": {:git, "https://akkoma.dev/AkkomaGang/file_ex.git", "cc7067c7d446c2526e9ecf91d40896b088851569", [ref: "cc7067c7d446c2526e9ecf91d40896b088851569"]},
|
||||
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
||||
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
||||
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
|
||||
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
|
||||
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
|
||||
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
|
||||
"gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"},
|
||||
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
|
||||
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
|
||||
"hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
||||
"http_signatures": {:git, "https://akkoma.dev/AkkomaGang/http_signatures.git", "d44c43d66758c6a73eaa4da9cffdbee0c5da44ae", [ref: "d44c43d66758c6a73eaa4da9cffdbee0c5da44ae"]},
|
||||
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"igniter": {:hex, :igniter, "0.5.29", "6bf7ddaf15e88ae75f6dad514329530ec8f4721ba14782f6386a7345c1be99fd", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "beb6e0f69fc6d4e3975ffa26c5459fc63fd96f85cfaeba984c2dfd3d7333b6ad"},
|
||||
"inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
"joken": {:hex, :joken, "2.6.1", "2ca3d8d7f83bf7196296a3d9b2ecda421a404634bfc618159981a960020480a1", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "ab26122c400b3d254ce7d86ed066d6afad27e70416df947cdcb01e13a7382e68"},
|
||||
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
|
||||
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
|
||||
"jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
|
||||
"linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"},
|
||||
"mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"},
|
||||
"mail": {:hex, :mail, "0.4.3", "16df84500780980826d9b52059d24c08fdd75c98f178a7a7ea809ea83fb70542", [:mix], [], "hexpm", "164975550b977e47cab431c403b0e90c8ce542036d32c7189b83839d8d7d391b"},
|
||||
"majic": {:git, "https://akkoma.dev/AkkomaGang/majic.git", "80540b36939ec83f48e76c61e5000e0fd67706f0", [ref: "80540b36939ec83f48e76c61e5000e0fd67706f0"]},
|
||||
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
|
||||
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
|
||||
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "b21ab7754024af096f2d14247574f55f0063295b", [ref: "b21ab7754024af096f2d14247574f55f0063295b"]},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "360a30267a847810a63ab48f606ba227b2ca05f0", [ref: "360a30267a847810a63ab48f606ba227b2ca05f0"]},
|
||||
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
|
||||
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
|
||||
"mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"},
|
||||
"mneme": {:hex, :mneme, "0.10.2", "f263ff74e993ef9eb160e04b608a149881e601ed6f5508c9afcafb578a184b52", [:mix], [{:file_system, "~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:igniter, "~> 0.3.76 or ~> 0.4.0 or ~> 0.5.0", [hex: :igniter, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:owl, "~> 0.9", [hex: :owl, repo: "hexpm", optional: false]}, {:rewrite, "~> 1.0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "3b9493fc114c4bb0f6232e021620ffd7944819b9b9105a5b286b6dc907f7720a"},
|
||||
"mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"},
|
||||
"mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"},
|
||||
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
|
||||
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||
"nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"oban": {:hex, :oban, "2.17.10", "c3e5bd739b5c3fdc38eba1d43ab270a8c6ca4463bb779b7705c69400b0d87678", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4afd027b8e2bc3c399b54318b4f46ee8c40251fb55a285cb4e38b5363f0ee7c4"},
|
||||
"open_api_spex": {:hex, :open_api_spex, "3.19.1", "65ccb5d06e3d664d1eec7c5ea2af2289bd2f37897094a74d7219fb03fc2b5994", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "392895827ce2984a3459c91a484e70708132d8c2c6c5363972b4b91d6bbac3dd"},
|
||||
"oban": {:hex, :oban, "2.19.2", "11e635c49b4f422814eb96a4d78974c9e67d62a20a969e5fd50db040d32c08ba", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de8314b00b31d17f98fd2c76781f80c1cfc8621122b41830c0834486c44e1087"},
|
||||
"oban_met": {:hex, :oban_met, "1.0.1", "737db0064567b923d3f35efd1d3009dd1435d60ee6f98dbb55dbb83db8f4f4fa", [:mix], [{:oban, "~> 2.18", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "0492d841f880b76c5b73081bc70ebea20ebacc08e871345f72c2270513f09957"},
|
||||
"oban_web": {:hex, :oban_web, "2.11.1", "646492d92177a43ff869535f0c92380e17148f45bc2c8dc8f74c69f8a2874bad", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.0", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "d853c6af3f7c20d03a2bf7b1baad71835d50fbb98af05004e9b51da558b90b01"},
|
||||
"open_api_spex": {:hex, :open_api_spex, "3.21.2", "6a704f3777761feeb5657340250d6d7332c545755116ca98f33d4b875777e1e5", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "f42ae6ed668b895ebba3e02773cfb4b41050df26f803f2ef634c72a7687dc387"},
|
||||
"owl": {:hex, :owl, "0.12.2", "65906b525e5c3ef51bab6cba7687152be017aebe1da077bb719a5ee9f7e60762", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "6398efa9e1fea70a04d24231e10dcd66c1ac1aa2da418d20ef5357ec61de2880"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.20", "6bababaf27d59f5628f9b608de902a021be2cecefb8231e1dbdc0a2e2e480e9b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "6be2ab98302e8784a31829e0d50d8bdfa81a23cd912c395bafd8b8bfb5a086c2"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.18", "1f38fbd7c363723f19aad1a04b5490ff3a178e37daaf6999594d5f34796c47fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a5810d0472f3189ede6d2a95bda7f31c6113156b91784a3426cb0ab6a6d85214"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.5", "f072166f87c44ffaf2b47b65c5ced8c375797830e517bfcf0a006fe7eb113911", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "94abbc84df8a93a64514fc41528695d7326b6f3095e906b32f264ec4280811f3"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
||||
"plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"},
|
||||
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
|
||||
"poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
|
||||
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
|
||||
"postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
|
||||
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
|
||||
"pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
|
||||
"remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"},
|
||||
"req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"},
|
||||
"rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"},
|
||||
"search_parser": {:git, "https://github.com/FloatingGhost/pleroma-contrib-search-parser.git", "08971a81e68686f9ac465cfb6661d51c5e4e1e7f", [ref: "08971a81e68686f9ac465cfb6661d51c5e4e1e7f"]},
|
||||
"sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
|
||||
"sourceror": {:hex, :sourceror, "1.7.1", "599d78f4cc2be7d55c9c4fd0a8d772fd0478e3a50e726697c20d13d02aa056d4", [:mix], [], "hexpm", "cd6f268fe29fa00afbc535e215158680a0662b357dc784646d7dff28ac65a0fc"},
|
||||
"spitfire": {:hex, :spitfire, "0.1.5", "10b041e781bff9544d2fdf00893e1a325758408c5366a9bfa4333072568659b1", [:mix], [], "hexpm", "866a55d21fe827934ff38200111335c9dd311df13cbf2580ed71d84b0a783150"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
||||
"swoosh": {:hex, :swoosh, "1.14.4", "94e9dba91f7695a10f49b0172c4a4cb658ef24abef7e8140394521b7f3bbb2d4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "081c5a590e4ba85cc89baddf7b2beecf6c13f7f84a958f1cd969290815f0f026"},
|
||||
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
|
||||
"table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"},
|
||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
|
||||
"telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"},
|
||||
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
|
||||
"temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
|
||||
"tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"},
|
||||
"tesla": {:hex, :tesla, "1.14.1", "71c5b031b4e089c0fbfb2b362e24b4478465773ae4ef569760a8c2899ad1e73c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c1dde8140a49a3bef5bb622356e77ac5a24ad0c8091f12c3b7fc1077ce797155"},
|
||||
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
|
||||
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
||||
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"},
|
||||
"ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
|
||||
"vex": {:hex, :vex, "0.9.2", "fe061acc9e0907d983d46b51bf35d58176f0fe6eb7ba3b33c9336401bf42b6d1", [:mix], [], "hexpm", "76e709a9762e98c6b462dfce92e9b5dfbf712839227f2da8add6dd11549b12cb"},
|
||||
"web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
|
||||
"websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
defmodule Pleroma.Repo.Migrations.CreateSigningKeyTable do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:signing_keys, primary_key: false) do
|
||||
add :user_id, references(:users, type: :uuid, on_delete: :delete_all)
|
||||
add :key_id, :text, primary_key: true
|
||||
add :public_key, :text
|
||||
add :private_key, :text
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
end
|
38
priv/repo/migrations/20240625220752_move_signing_keys.exs
Normal file
38
priv/repo/migrations/20240625220752_move_signing_keys.exs
Normal file
|
@ -0,0 +1,38 @@
|
|||
defmodule Pleroma.Repo.Migrations.MoveSigningKeys do
|
||||
use Ecto.Migration
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Repo
|
||||
import Ecto.Query
|
||||
|
||||
def up do
|
||||
# we do not handle remote users here!
|
||||
# because we want to store a key id -> user id mapping, and we don't
|
||||
# currently store key ids for remote users...
|
||||
# Also this MUST use select, else the migration will fail in future installs with new user fields!
|
||||
from(u in Pleroma.User,
|
||||
where: u.local == true,
|
||||
select: {u.id, fragment("?.keys", u), u.ap_id}
|
||||
)
|
||||
|> Repo.stream(timeout: :infinity)
|
||||
|> Enum.each(fn
|
||||
{user_id, private_key, ap_id} ->
|
||||
IO.puts("Migrating user #{user_id}")
|
||||
# we can precompute the public key here...
|
||||
# we do use it on every user view which makes it a bit of a dos attack vector
|
||||
# so we should probably cache it
|
||||
{:ok, public_key} = User.SigningKey.private_pem_to_public_pem(private_key)
|
||||
|
||||
key = %User.SigningKey{
|
||||
user_id: user_id,
|
||||
public_key: public_key,
|
||||
key_id: User.SigningKey.local_key_id(ap_id),
|
||||
private_key: private_key
|
||||
}
|
||||
|
||||
{:ok, _} = Repo.insert(key)
|
||||
end)
|
||||
end
|
||||
|
||||
# no need to rollback
|
||||
def down, do: :ok
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddSigningKeyIndex do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create_if_not_exists(index(:signing_keys, [:user_id], name: :signing_keys_user_id_index))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
# Akkoma: Magically expressive social media
|
||||
# Copyright © 2024 Akkoma Authors <https://akkoma.dev/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Repo.Migrations.RemoteUserCountEstimateFunction do
|
||||
use Ecto.Migration
|
||||
|
||||
@function_name "estimate_remote_user_count"
|
||||
|
||||
def up() do
|
||||
# yep, this EXPLAIN (ab)use is blessed by the PostgreSQL wiki:
|
||||
# https://wiki.postgresql.org/wiki/Count_estimate
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION #{@function_name}()
|
||||
RETURNS integer
|
||||
LANGUAGE plpgsql AS $$
|
||||
DECLARE plan jsonb;
|
||||
BEGIN
|
||||
EXECUTE '
|
||||
EXPLAIN (FORMAT JSON)
|
||||
SELECT *
|
||||
FROM public.users
|
||||
WHERE local = false AND
|
||||
is_active = true AND
|
||||
invisible = false AND
|
||||
nickname IS NOT NULL;
|
||||
' INTO plan;
|
||||
RETURN plan->0->'Plan'->'Plan Rows';
|
||||
END;
|
||||
$$;
|
||||
"""
|
||||
|> execute()
|
||||
end
|
||||
|
||||
def down() do
|
||||
execute("DROP FUNCTION IF EXISTS #{@function_name}()")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Repo.Migrations.RemoveUserApEnabled do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
remove(:ap_enabled, :boolean, default: false, null: false)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
defmodule Pleroma.Repo.Migrations.SigningKeyNullability do
|
||||
use Ecto.Migration
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
def up() do
|
||||
# Delete existing NULL entries; they are useless
|
||||
Pleroma.User.SigningKey
|
||||
|> where([s], is_nil(s.user_id) or is_nil(s.public_key))
|
||||
|> Pleroma.Repo.delete_all()
|
||||
|
||||
alter table(:signing_keys) do
|
||||
modify :user_id, :uuid, null: false
|
||||
modify :public_key, :text, null: false
|
||||
end
|
||||
end
|
||||
|
||||
def down() do
|
||||
alter table(:signing_keys) do
|
||||
modify :user_id, :uuid, null: true
|
||||
modify :public_key, :text, null: true
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue